cmdx 1.21.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 +118 -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 -237
- data/lib/cmdx/context.rb +264 -243
- 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 +247 -524
- 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 -200
- data/lib/cmdx/signal.rb +165 -0
- data/lib/cmdx/task.rb +443 -343
- 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 +71 -96
- 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 +120 -48
- 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 +8 -7
- 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 -378
- 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 -77
- 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 -55
- data/lib/locales/ar.yml +0 -55
- data/lib/locales/az.yml +0 -55
- data/lib/locales/be.yml +0 -55
- data/lib/locales/bg.yml +0 -55
- data/lib/locales/bn.yml +0 -55
- data/lib/locales/bs.yml +0 -55
- data/lib/locales/ca.yml +0 -55
- data/lib/locales/cnr.yml +0 -55
- data/lib/locales/cs.yml +0 -55
- data/lib/locales/cy.yml +0 -55
- data/lib/locales/da.yml +0 -55
- data/lib/locales/de.yml +0 -55
- data/lib/locales/dz.yml +0 -55
- data/lib/locales/el.yml +0 -55
- data/lib/locales/eo.yml +0 -55
- data/lib/locales/es.yml +0 -55
- data/lib/locales/et.yml +0 -55
- data/lib/locales/eu.yml +0 -55
- data/lib/locales/fa.yml +0 -55
- data/lib/locales/fi.yml +0 -55
- data/lib/locales/fr.yml +0 -55
- data/lib/locales/fy.yml +0 -55
- data/lib/locales/gd.yml +0 -55
- data/lib/locales/gl.yml +0 -55
- data/lib/locales/he.yml +0 -55
- data/lib/locales/hi.yml +0 -55
- data/lib/locales/hr.yml +0 -55
- data/lib/locales/hu.yml +0 -55
- data/lib/locales/hy.yml +0 -55
- data/lib/locales/id.yml +0 -55
- data/lib/locales/is.yml +0 -55
- data/lib/locales/it.yml +0 -55
- data/lib/locales/ja.yml +0 -55
- data/lib/locales/ka.yml +0 -55
- data/lib/locales/kk.yml +0 -55
- data/lib/locales/km.yml +0 -55
- data/lib/locales/kn.yml +0 -55
- data/lib/locales/ko.yml +0 -55
- data/lib/locales/lb.yml +0 -55
- data/lib/locales/lo.yml +0 -55
- data/lib/locales/lt.yml +0 -55
- data/lib/locales/lv.yml +0 -55
- data/lib/locales/mg.yml +0 -55
- data/lib/locales/mk.yml +0 -55
- data/lib/locales/ml.yml +0 -55
- data/lib/locales/mn.yml +0 -55
- data/lib/locales/mr-IN.yml +0 -55
- data/lib/locales/ms.yml +0 -55
- data/lib/locales/nb.yml +0 -55
- data/lib/locales/ne.yml +0 -55
- data/lib/locales/nl.yml +0 -55
- data/lib/locales/nn.yml +0 -55
- data/lib/locales/oc.yml +0 -55
- data/lib/locales/or.yml +0 -55
- data/lib/locales/pa.yml +0 -55
- data/lib/locales/pl.yml +0 -55
- data/lib/locales/pt.yml +0 -55
- data/lib/locales/rm.yml +0 -55
- data/lib/locales/ro.yml +0 -55
- data/lib/locales/ru.yml +0 -55
- data/lib/locales/sc.yml +0 -55
- data/lib/locales/sk.yml +0 -55
- data/lib/locales/sl.yml +0 -55
- data/lib/locales/sq.yml +0 -55
- data/lib/locales/sr.yml +0 -55
- data/lib/locales/st.yml +0 -55
- data/lib/locales/sv.yml +0 -55
- data/lib/locales/sw.yml +0 -55
- data/lib/locales/ta.yml +0 -55
- data/lib/locales/te.yml +0 -55
- data/lib/locales/th.yml +0 -55
- data/lib/locales/tl.yml +0 -55
- data/lib/locales/tr.yml +0 -55
- data/lib/locales/tt.yml +0 -55
- data/lib/locales/ug.yml +0 -55
- data/lib/locales/uk.yml +0 -55
- data/lib/locales/ur.yml +0 -55
- data/lib/locales/uz.yml +0 -55
- data/lib/locales/vi.yml +0 -55
- data/lib/locales/wo.yml +0 -55
- data/lib/locales/zh-CN.yml +0 -55
- data/lib/locales/zh-HK.yml +0 -55
- data/lib/locales/zh-TW.yml +0 -55
- data/lib/locales/zh-YUE.yml +0 -55
data/lib/cmdx/chain.rb
CHANGED
|
@@ -1,215 +1,118 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# Ordered collection of {Result}s produced by a top-level task and any nested
|
|
5
|
+
# tasks it triggers. A Chain is stored per-fiber so concurrent workflows
|
|
6
|
+
# (see Pipeline parallel strategy) each get their own. The root Runtime
|
|
7
|
+
# clears the chain on teardown.
|
|
7
8
|
class Chain
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
include Enumerable
|
|
10
11
|
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# Returns the unique identifier for this chain.
|
|
15
|
-
#
|
|
16
|
-
# @return [String] The chain identifier
|
|
17
|
-
#
|
|
18
|
-
# @example
|
|
19
|
-
# chain.id # => "abc123xyz"
|
|
20
|
-
#
|
|
21
|
-
# @rbs @id: String
|
|
22
|
-
attr_reader :id
|
|
23
|
-
|
|
24
|
-
# Returns the collection of execution results in this chain.
|
|
25
|
-
#
|
|
26
|
-
# @return [Array<Result>] Array of task results
|
|
27
|
-
#
|
|
28
|
-
# @example
|
|
29
|
-
# chain.results # => [#<Result>, #<Result>]
|
|
30
|
-
#
|
|
31
|
-
# @rbs @results: Array[Result]
|
|
32
|
-
attr_reader :results
|
|
33
|
-
|
|
34
|
-
def_delegators :results, :first, :last, :size
|
|
35
|
-
def_delegators :first, :state, :status, :outcome
|
|
36
|
-
|
|
37
|
-
# Creates a new chain with a unique identifier and empty results collection.
|
|
38
|
-
#
|
|
39
|
-
# @return [Chain] A new chain instance
|
|
40
|
-
#
|
|
41
|
-
# @rbs () -> void
|
|
42
|
-
def initialize(dry_run: false)
|
|
43
|
-
@mutex = Mutex.new
|
|
44
|
-
@id = Identifier.generate
|
|
45
|
-
@results = []
|
|
46
|
-
@dry_run = !!dry_run
|
|
47
|
-
end
|
|
12
|
+
# Fiber-local storage key used by {.current}/{.current=}/{.clear}.
|
|
13
|
+
STORAGE_KEY = :cmdx_chain
|
|
48
14
|
|
|
49
15
|
class << self
|
|
50
16
|
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
# @return [Chain, nil] The current chain or nil if none exists
|
|
54
|
-
#
|
|
55
|
-
# @example
|
|
56
|
-
# chain = Chain.current
|
|
57
|
-
# if chain
|
|
58
|
-
# puts "Current chain: #{chain.id}"
|
|
59
|
-
# end
|
|
60
|
-
#
|
|
61
|
-
# @rbs () -> Chain?
|
|
17
|
+
# @return [Chain, nil] the chain active on the current fiber, or nil outside execution
|
|
62
18
|
def current
|
|
63
|
-
|
|
19
|
+
Fiber[STORAGE_KEY]
|
|
64
20
|
end
|
|
65
21
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
# @
|
|
69
|
-
#
|
|
70
|
-
# @return [Chain] The set chain
|
|
71
|
-
#
|
|
72
|
-
# @example
|
|
73
|
-
# Chain.current = my_chain
|
|
74
|
-
#
|
|
75
|
-
# @rbs (Chain chain) -> Chain
|
|
22
|
+
# Installs `chain` as the active chain on the current fiber.
|
|
23
|
+
# @param chain [Chain, nil]
|
|
24
|
+
# @return [Chain, nil]
|
|
76
25
|
def current=(chain)
|
|
77
|
-
|
|
26
|
+
Fiber[STORAGE_KEY] = chain
|
|
78
27
|
end
|
|
79
28
|
|
|
80
|
-
# Clears the
|
|
81
|
-
#
|
|
82
|
-
# @return [nil] Always returns nil
|
|
83
|
-
#
|
|
84
|
-
# @example
|
|
85
|
-
# Chain.clear
|
|
86
|
-
#
|
|
87
|
-
# @rbs () -> nil
|
|
29
|
+
# Clears the fiber-local chain reference.
|
|
30
|
+
# @return [nil]
|
|
88
31
|
def clear
|
|
89
|
-
|
|
32
|
+
Fiber[STORAGE_KEY] = nil
|
|
90
33
|
end
|
|
91
34
|
|
|
92
|
-
|
|
93
|
-
# Creates a new chain if none exists, otherwise appends to the current one.
|
|
94
|
-
#
|
|
95
|
-
# @param result [Result] The task execution result to add
|
|
96
|
-
#
|
|
97
|
-
# @return [Chain] The current chain (newly created or existing)
|
|
98
|
-
#
|
|
99
|
-
# @raise [TypeError] If result is not a CMDx::Result instance
|
|
100
|
-
#
|
|
101
|
-
# @example
|
|
102
|
-
# result = task.execute
|
|
103
|
-
# chain = Chain.build(result)
|
|
104
|
-
# puts "Chain size: #{chain.size}"
|
|
105
|
-
#
|
|
106
|
-
# @rbs (Result result) -> Chain
|
|
107
|
-
def build(result, dry_run: false)
|
|
108
|
-
raise TypeError, "must be a CMDx::Result" unless result.is_a?(Result)
|
|
109
|
-
|
|
110
|
-
self.current ||= new(dry_run:)
|
|
111
|
-
current.push(result)
|
|
112
|
-
current
|
|
113
|
-
end
|
|
35
|
+
end
|
|
114
36
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
# Returns the thread or fiber storage for the current execution context.
|
|
118
|
-
#
|
|
119
|
-
# @return [Hash] The thread or fiber storage
|
|
120
|
-
#
|
|
121
|
-
# @rbs () -> Hash
|
|
122
|
-
if Fiber.respond_to?(:storage)
|
|
123
|
-
def thread_or_fiber = Fiber.storage
|
|
124
|
-
else
|
|
125
|
-
def thread_or_fiber = Thread.current
|
|
126
|
-
end
|
|
37
|
+
attr_reader :xid, :id, :results
|
|
127
38
|
|
|
39
|
+
# @param xid [String, nil] external correlation id (e.g. Rails `request_id`)
|
|
40
|
+
# shared across every {Result} in this chain. Resolved once by Runtime
|
|
41
|
+
# from {Configuration#xid} when the root chain is created.
|
|
42
|
+
def initialize(xid = nil)
|
|
43
|
+
@xid = xid
|
|
44
|
+
@id = SecureRandom.uuid_v7
|
|
45
|
+
@mutex = Mutex.new
|
|
46
|
+
@results = []
|
|
128
47
|
end
|
|
129
48
|
|
|
130
|
-
#
|
|
131
|
-
# Caches the result's index to avoid repeated O(n) lookups.
|
|
132
|
-
#
|
|
133
|
-
# @param result [Result] The result to append
|
|
49
|
+
# Appends `result` to the chain. Thread-safe to support parallel pipelines.
|
|
134
50
|
#
|
|
135
|
-
# @
|
|
136
|
-
#
|
|
137
|
-
# @rbs (Result result) -> Array[Result]
|
|
51
|
+
# @param result [Result]
|
|
52
|
+
# @return [Chain] self for chaining
|
|
138
53
|
def push(result)
|
|
139
|
-
@mutex.synchronize
|
|
140
|
-
|
|
141
|
-
@results << result
|
|
142
|
-
end
|
|
54
|
+
@mutex.synchronize { @results << result }
|
|
55
|
+
self
|
|
143
56
|
end
|
|
57
|
+
alias << push
|
|
144
58
|
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
# @param result [Result] The result to find
|
|
59
|
+
# Prepends `result` to the chain. Thread-safe to support parallel pipelines.
|
|
148
60
|
#
|
|
149
|
-
# @
|
|
150
|
-
#
|
|
151
|
-
|
|
61
|
+
# @param result [Result]
|
|
62
|
+
# @return [Chain] self for chaining
|
|
63
|
+
def unshift(result)
|
|
64
|
+
@mutex.synchronize { @results.unshift(result) }
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @param result [Result]
|
|
69
|
+
# @return [Integer, nil] zero-based position of `result`, or nil when absent
|
|
152
70
|
def index(result)
|
|
153
|
-
@
|
|
71
|
+
@results.index(result)
|
|
154
72
|
end
|
|
155
73
|
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
#
|
|
160
|
-
# @example
|
|
161
|
-
# chain.dry_run? # => true
|
|
162
|
-
#
|
|
163
|
-
# @rbs () -> bool
|
|
164
|
-
def dry_run?
|
|
165
|
-
!!@dry_run
|
|
74
|
+
# @return [Result, nil] the most recently appended result
|
|
75
|
+
def last
|
|
76
|
+
@results.last
|
|
166
77
|
end
|
|
167
78
|
|
|
168
|
-
#
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
#
|
|
172
|
-
# @example
|
|
173
|
-
# chain.freeze
|
|
174
|
-
# chain.results << result # => raises FrozenError
|
|
175
|
-
#
|
|
176
|
-
# @rbs () -> self
|
|
177
|
-
def freeze
|
|
178
|
-
results.freeze
|
|
179
|
-
super
|
|
79
|
+
# @return [Result, nil] the root result, or nil when absent
|
|
80
|
+
def root
|
|
81
|
+
@results.find(&:root?)
|
|
180
82
|
end
|
|
181
83
|
|
|
182
|
-
#
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
# @option return [Array<Hash>] :results Array of result hashes
|
|
186
|
-
#
|
|
187
|
-
# @return [Hash] Hash containing chain id and serialized results
|
|
188
|
-
#
|
|
189
|
-
# @example
|
|
190
|
-
# chain_hash = chain.to_h
|
|
191
|
-
# puts chain_hash[:id]
|
|
192
|
-
# puts chain_hash[:results].size
|
|
193
|
-
#
|
|
194
|
-
# @rbs () -> Hash[Symbol, untyped]
|
|
195
|
-
def to_h
|
|
196
|
-
{
|
|
197
|
-
id:,
|
|
198
|
-
dry_run: dry_run?,
|
|
199
|
-
results: results.map(&:to_h)
|
|
200
|
-
}
|
|
84
|
+
# @return [String, nil] the state of the root result, or nil when absent
|
|
85
|
+
def state
|
|
86
|
+
root&.state
|
|
201
87
|
end
|
|
202
88
|
|
|
203
|
-
#
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
#
|
|
89
|
+
# @return [String, nil] the status of the root result, or nil when absent
|
|
90
|
+
def status
|
|
91
|
+
root&.status
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @return [Boolean]
|
|
95
|
+
def empty?
|
|
96
|
+
@results.empty?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @return [Integer]
|
|
100
|
+
def size
|
|
101
|
+
@results.size
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @yield [Result] each result in insertion order
|
|
105
|
+
# @return [Enumerator, Chain]
|
|
106
|
+
def each(&)
|
|
107
|
+
@results.each(&)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Freezes the chain and its results. Called by Runtime teardown.
|
|
209
111
|
#
|
|
210
|
-
# @
|
|
211
|
-
def
|
|
212
|
-
|
|
112
|
+
# @return [Chain] self
|
|
113
|
+
def freeze
|
|
114
|
+
@mutex.synchronize { @results.freeze }
|
|
115
|
+
super
|
|
213
116
|
end
|
|
214
117
|
|
|
215
118
|
end
|
data/lib/cmdx/coercions/array.rb
CHANGED
|
@@ -1,47 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# Handles conversion from strings that look like JSON arrays and other
|
|
8
|
-
# values that can be wrapped in an array using Ruby's Array() method.
|
|
4
|
+
class Coercions
|
|
5
|
+
# Coerces to Array. JSON-decodes strings; arrays pass through; objects
|
|
6
|
+
# responding to `#to_a` are unwrapped; everything else is wrapped.
|
|
9
7
|
module Array
|
|
10
8
|
|
|
11
9
|
extend self
|
|
12
10
|
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# @
|
|
16
|
-
# @
|
|
17
|
-
# @option options [Object] :unused Currently no options are used
|
|
18
|
-
#
|
|
19
|
-
# @return [Array] The converted array value
|
|
20
|
-
#
|
|
21
|
-
# @raise [CoercionError] If the value cannot be converted to an array
|
|
22
|
-
#
|
|
23
|
-
# @example Convert a JSON-like string to an array
|
|
24
|
-
# Array.call("[1, 2, 3]") # => [1, 2, 3]
|
|
25
|
-
# @example Convert other values using Array()
|
|
26
|
-
# Array.call("hello") # => ["hello"]
|
|
27
|
-
# Array.call(42) # => [42]
|
|
28
|
-
# Array.call(nil) # => []
|
|
29
|
-
# @example Handle invalid JSON-like strings
|
|
30
|
-
# Array.call("[not json") # => raises CoercionError
|
|
31
|
-
#
|
|
32
|
-
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Array[untyped]
|
|
11
|
+
# @param value [Object]
|
|
12
|
+
# @param options [Hash{Symbol => Object}]
|
|
13
|
+
# @option options [Object] reserved for future per-coercion configuration (currently ignored)
|
|
14
|
+
# @return [Array, Coercions::Failure]
|
|
33
15
|
def call(value, options = EMPTY_HASH)
|
|
34
|
-
if value.is_a?(::
|
|
35
|
-
value
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
16
|
+
if value.is_a?(::Array)
|
|
17
|
+
value
|
|
18
|
+
elsif value.is_a?(::String)
|
|
19
|
+
result = JSON.parse(value)
|
|
20
|
+
result.is_a?(::Array) ? result : [value]
|
|
21
|
+
elsif value.respond_to?(:to_a)
|
|
22
|
+
value.to_a
|
|
39
23
|
else
|
|
40
|
-
|
|
24
|
+
[value]
|
|
41
25
|
end
|
|
42
26
|
rescue JSON::ParserError
|
|
43
|
-
|
|
44
|
-
|
|
27
|
+
[value]
|
|
28
|
+
rescue TypeError
|
|
29
|
+
type = I18nProxy.t("cmdx.types.array")
|
|
30
|
+
Failure.new(I18nProxy.t("cmdx.coercions.into_an", type:))
|
|
45
31
|
end
|
|
46
32
|
|
|
47
33
|
end
|
|
@@ -1,41 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# Handles conversion from numeric strings, integers, floats, and other
|
|
8
|
-
# values that can be converted to BigDecimal using Ruby's BigDecimal() method.
|
|
4
|
+
class Coercions
|
|
5
|
+
# Coerces to `BigDecimal`. Default precision is 14 digits; override with
|
|
6
|
+
# `precision:` on the declaration.
|
|
9
7
|
module BigDecimal
|
|
10
8
|
|
|
11
9
|
extend self
|
|
12
10
|
|
|
13
|
-
# @
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# @param value [Object] The value to convert to BigDecimal
|
|
19
|
-
# @param options [Hash] Optional configuration parameters
|
|
20
|
-
# @option options [Integer] :precision The precision to use (defaults to DEFAULT_PRECISION)
|
|
21
|
-
#
|
|
22
|
-
# @return [BigDecimal] The converted BigDecimal value
|
|
23
|
-
#
|
|
24
|
-
# @raise [CoercionError] If the value cannot be converted to BigDecimal
|
|
25
|
-
#
|
|
26
|
-
# @example Convert numeric strings to BigDecimal
|
|
27
|
-
# BigDecimal.call("123.45") # => #<BigDecimal:7f8b8c0d8e0f '0.12345E3',9(18)>
|
|
28
|
-
# BigDecimal.call("0.001", precision: 6) # => #<BigDecimal:7f8b8c0d8e0f '0.1E-2',9(18)>
|
|
29
|
-
# @example Convert other numeric types
|
|
30
|
-
# BigDecimal.call(42) # => #<BigDecimal:7f8b8c0d8e0f '0.42E2',9(18)>
|
|
31
|
-
# BigDecimal.call(3.14159) # => #<BigDecimal:7f8b8c0d8e0f '0.314159E1',9(18)>
|
|
32
|
-
#
|
|
33
|
-
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> BigDecimal
|
|
11
|
+
# @param value [Object]
|
|
12
|
+
# @param options [Hash{Symbol => Object}]
|
|
13
|
+
# @option options [Integer] :precision (14)
|
|
14
|
+
# @return [BigDecimal, Coercions::Failure]
|
|
34
15
|
def call(value, options = EMPTY_HASH)
|
|
35
|
-
|
|
16
|
+
return value if value.is_a?(BigDecimal)
|
|
17
|
+
|
|
18
|
+
BigDecimal(value, options[:precision] || 14)
|
|
36
19
|
rescue ArgumentError, TypeError
|
|
37
|
-
type =
|
|
38
|
-
|
|
20
|
+
type = I18nProxy.t("cmdx.types.big_decimal")
|
|
21
|
+
Failure.new(I18nProxy.t("cmdx.coercions.into_a", type:))
|
|
39
22
|
end
|
|
40
23
|
|
|
41
24
|
end
|
|
@@ -1,56 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# using predefined truthy and falsey patterns.
|
|
4
|
+
class Coercions
|
|
5
|
+
# Coerces to Boolean by matching the string form against the {TRUTHY}
|
|
6
|
+
# and {FALSEY} sets (case- and whitespace-insensitive). Anything else
|
|
7
|
+
# (including `nil`) fails.
|
|
9
8
|
module Boolean
|
|
10
9
|
|
|
11
10
|
extend self
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
FALSEY =
|
|
15
|
-
|
|
16
|
-
# @
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# @param value [Object] The value to convert to boolean
|
|
22
|
-
# @param options [Hash] Optional configuration parameters (currently unused)
|
|
23
|
-
# @option options [Object] :unused Currently no options are used
|
|
24
|
-
#
|
|
25
|
-
# @return [Boolean] The converted boolean value
|
|
26
|
-
#
|
|
27
|
-
# @raise [CoercionError] If the value cannot be converted to boolean
|
|
28
|
-
#
|
|
29
|
-
# @example Convert truthy strings to true
|
|
30
|
-
# Boolean.call("true") # => true
|
|
31
|
-
# Boolean.call("yes") # => true
|
|
32
|
-
# Boolean.call("1") # => true
|
|
33
|
-
# @example Convert falsey strings to false
|
|
34
|
-
# Boolean.call("false") # => false
|
|
35
|
-
# Boolean.call("no") # => false
|
|
36
|
-
# Boolean.call("0") # => false
|
|
37
|
-
# Boolean.call(nil) # => false
|
|
38
|
-
# Boolean.call("") # => false
|
|
39
|
-
# @example Handle case-insensitive input
|
|
40
|
-
# Boolean.call("TRUE") # => true
|
|
41
|
-
# Boolean.call("False") # => false
|
|
42
|
-
# @example Handle edge cases
|
|
43
|
-
# Boolean.call("abc") # => raises CoercionError
|
|
44
|
-
#
|
|
45
|
-
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> bool
|
|
12
|
+
TRUTHY = Set["true", "yes", "on", "y", "1", "t"].freeze
|
|
13
|
+
FALSEY = Set["false", "no", "off", "n", "0", "f"].freeze
|
|
14
|
+
|
|
15
|
+
# @param value [Object]
|
|
16
|
+
# @param options [Hash{Symbol => Object}]
|
|
17
|
+
# @option options [Object] reserved for future per-coercion configuration (currently ignored)
|
|
18
|
+
# @return [Boolean, Coercions::Failure]
|
|
46
19
|
def call(value, options = EMPTY_HASH)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
20
|
+
return coercion_failure if value.nil?
|
|
21
|
+
|
|
22
|
+
str = value.to_s.strip.downcase
|
|
23
|
+
return true if TRUTHY.include?(str)
|
|
24
|
+
return false if FALSEY.include?(str)
|
|
25
|
+
|
|
26
|
+
coercion_failure
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def coercion_failure
|
|
32
|
+
type = I18nProxy.t("cmdx.types.boolean")
|
|
33
|
+
Failure.new(I18nProxy.t("cmdx.coercions.into_a", type:))
|
|
54
34
|
end
|
|
55
35
|
|
|
56
36
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
class Coercions
|
|
5
|
+
# Invokes an inline `:coerce` handler (Symbol method name, Proc, or
|
|
6
|
+
# anything with `#call`). Used by {Coercions#coerce} for non-built-in
|
|
7
|
+
# rules.
|
|
8
|
+
module Coerce
|
|
9
|
+
|
|
10
|
+
extend self
|
|
11
|
+
|
|
12
|
+
# @param task [Task] receiver for Symbol/Proc handlers, also passed to callable handlers
|
|
13
|
+
# @param value [Object]
|
|
14
|
+
# @param handler [Symbol, Proc, #call]
|
|
15
|
+
# @return [Object] the handler's return value
|
|
16
|
+
# @raise [ArgumentError] when `handler` isn't a supported type
|
|
17
|
+
def call(task, value, handler)
|
|
18
|
+
case handler
|
|
19
|
+
when ::Symbol
|
|
20
|
+
task.send(handler, value)
|
|
21
|
+
when ::Proc
|
|
22
|
+
task.instance_exec(value, &handler)
|
|
23
|
+
else
|
|
24
|
+
return handler.call(value, task) if handler.respond_to?(:call)
|
|
25
|
+
|
|
26
|
+
raise ArgumentError, "coerce handler must be a Symbol, Proc, or respond to #call"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -1,39 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# Handles conversion from numeric strings, integers, floats, and other
|
|
8
|
-
# values that can be converted to Complex using Ruby's Complex() method.
|
|
4
|
+
class Coercions
|
|
5
|
+
# Coerces to `Complex`. Supply `imaginary:` to provide the imaginary
|
|
6
|
+
# component when `value` is a real-only input.
|
|
9
7
|
module Complex
|
|
10
8
|
|
|
11
9
|
extend self
|
|
12
10
|
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# @
|
|
16
|
-
# @
|
|
17
|
-
# @option options [Object] :* Any configuration option (unused)
|
|
18
|
-
#
|
|
19
|
-
# @return [Complex] The converted Complex number value
|
|
20
|
-
#
|
|
21
|
-
# @raise [CoercionError] If the value cannot be converted to Complex
|
|
22
|
-
#
|
|
23
|
-
# @example Convert numeric strings to Complex
|
|
24
|
-
# Complex.call("3+4i") # => (3+4i)
|
|
25
|
-
# Complex.call("2.5") # => (2.5+0i)
|
|
26
|
-
# @example Convert other numeric types
|
|
27
|
-
# Complex.call(5) # => (5+0i)
|
|
28
|
-
# Complex.call(3.14) # => (3.14+0i)
|
|
29
|
-
# Complex.call(Complex(1, 2)) # => (1+2i)
|
|
30
|
-
#
|
|
31
|
-
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Complex
|
|
11
|
+
# @param value [Object]
|
|
12
|
+
# @param options [Hash{Symbol => Object}]
|
|
13
|
+
# @option options [Numeric] :imaginary (0)
|
|
14
|
+
# @return [Complex, Coercions::Failure]
|
|
32
15
|
def call(value, options = EMPTY_HASH)
|
|
33
|
-
Complex
|
|
16
|
+
return value if value.is_a?(::Complex)
|
|
17
|
+
|
|
18
|
+
Complex(value, options[:imaginary] || 0)
|
|
34
19
|
rescue ArgumentError, TypeError
|
|
35
|
-
type =
|
|
36
|
-
|
|
20
|
+
type = I18nProxy.t("cmdx.types.complex")
|
|
21
|
+
Failure.new(I18nProxy.t("cmdx.coercions.into_a", type:))
|
|
37
22
|
end
|
|
38
23
|
|
|
39
24
|
end
|
data/lib/cmdx/coercions/date.rb
CHANGED
|
@@ -1,45 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# and other date-like values to Date objects using Ruby's built-in parsing
|
|
9
|
-
# capabilities and optional custom format parsing.
|
|
4
|
+
class Coercions
|
|
5
|
+
# Coerces to `Date`. Pass `strptime:` to parse via a specific format;
|
|
6
|
+
# otherwise `Date.parse` is used for strings, and `#to_date` for any
|
|
7
|
+
# other responding object.
|
|
10
8
|
module Date
|
|
11
9
|
|
|
12
10
|
extend self
|
|
13
11
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# @
|
|
17
|
-
# @
|
|
18
|
-
# @option options [String] :strptime Custom date format string for parsing
|
|
19
|
-
#
|
|
20
|
-
# @return [Date] The converted Date object
|
|
21
|
-
#
|
|
22
|
-
# @raise [CoercionError] If the value cannot be converted to a Date
|
|
23
|
-
#
|
|
24
|
-
# @example Convert string to Date using default parsing
|
|
25
|
-
# Date.call("2023-12-25") # => #<Date: 2023-12-25>
|
|
26
|
-
# Date.call("Dec 25, 2023") # => #<Date: 2023-12-25>
|
|
27
|
-
# @example Convert string using custom format
|
|
28
|
-
# Date.call("25/12/2023", strptime: "%d/%m/%Y") # => #<Date: 2023-12-25>
|
|
29
|
-
# Date.call("12-25-2023", strptime: "%m-%d-%Y") # => #<Date: 2023-12-25>
|
|
30
|
-
# @example Return existing Date objects unchanged
|
|
31
|
-
# Date.call(Date.new(2023, 12, 25)) # => #<Date: 2023-12-25>
|
|
32
|
-
# Date.call(DateTime.new(2023, 12, 25)) # => #<Date: 2023-12-25>
|
|
33
|
-
#
|
|
34
|
-
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Date
|
|
12
|
+
# @param value [Object]
|
|
13
|
+
# @param options [Hash{Symbol => Object}]
|
|
14
|
+
# @option options [String] :strptime format string for `Date.strptime`
|
|
15
|
+
# @return [Date, Coercions::Failure]
|
|
35
16
|
def call(value, options = EMPTY_HASH)
|
|
36
|
-
|
|
37
|
-
|
|
17
|
+
if value.is_a?(::Date)
|
|
18
|
+
value
|
|
19
|
+
elsif value.is_a?(::String)
|
|
20
|
+
if (strptime = options[:strptime])
|
|
21
|
+
::Date.strptime(value, strptime)
|
|
22
|
+
else
|
|
23
|
+
::Date.parse(value)
|
|
24
|
+
end
|
|
25
|
+
elsif value.respond_to?(:to_date)
|
|
26
|
+
value.to_date
|
|
27
|
+
else
|
|
28
|
+
coercion_failure
|
|
29
|
+
end
|
|
30
|
+
rescue ArgumentError, TypeError, ::Date::Error
|
|
31
|
+
coercion_failure
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
raise CoercionError, Locale.t("cmdx.coercions.into_a", type:)
|
|
36
|
+
def coercion_failure
|
|
37
|
+
type = I18nProxy.t("cmdx.types.date")
|
|
38
|
+
Failure.new(I18nProxy.t("cmdx.coercions.into_a", type:))
|
|
43
39
|
end
|
|
44
40
|
|
|
45
41
|
end
|