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/inputs.rb
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
# Registry of declared task inputs. Each registration creates an {Input} and
|
|
5
|
+
# defines a reader method on the task class. {#resolve} walks every input
|
|
6
|
+
# (and nested children) to populate the task's instance variables before `work`.
|
|
7
|
+
class Inputs
|
|
8
|
+
|
|
9
|
+
attr_reader :registry
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@registry = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param source [Inputs] registry to duplicate
|
|
16
|
+
# @return [void]
|
|
17
|
+
def initialize_copy(source)
|
|
18
|
+
@registry = source.registry.dup
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Declares one or more inputs and defines accessor readers on `klass`.
|
|
22
|
+
# A block nests child inputs under each declared name (see {ChildBuilder}).
|
|
23
|
+
#
|
|
24
|
+
# @param klass [Class] the task class to define readers on
|
|
25
|
+
# @param names [Array<Symbol>] input names
|
|
26
|
+
# @param block [#call, nil] nested-input DSL (see {ChildBuilder})
|
|
27
|
+
# @param options [Hash{Symbol => Object}] passed to {Input#initialize}
|
|
28
|
+
# @option options [String] :description (also accepts `:desc`)
|
|
29
|
+
# @option options [Symbol] :as overrides the accessor name
|
|
30
|
+
# @option options [Boolean, String] :prefix prefix for the accessor name
|
|
31
|
+
# @option options [Boolean, String] :suffix suffix for the accessor name
|
|
32
|
+
# @option options [Symbol, Proc, #call] :source (`:context`) where to fetch from
|
|
33
|
+
# @option options [Object, Symbol, Proc, #call] :default
|
|
34
|
+
# @option options [Symbol, Proc, #call] :transform mutator applied after coercion
|
|
35
|
+
# @option options [Symbol, Proc, #call] :if
|
|
36
|
+
# @option options [Symbol, Proc, #call] :unless
|
|
37
|
+
# @option options [Boolean] :required
|
|
38
|
+
# @option options [Object] :coerce forwarded with declaration (see {Coercions#extract})
|
|
39
|
+
# @option options [Object] :validate forwarded with declaration (see {Validators#extract})
|
|
40
|
+
# @return [Inputs] self for chaining
|
|
41
|
+
# @yield block evaluated in a {ChildBuilder} for nested inputs
|
|
42
|
+
def register(klass, *names, **options, &block)
|
|
43
|
+
children = block ? ChildBuilder.build(&block) : EMPTY_ARRAY
|
|
44
|
+
|
|
45
|
+
names.each do |name|
|
|
46
|
+
input = Input.new(name, children:, **options)
|
|
47
|
+
registry[input.name] = input
|
|
48
|
+
klass.send(:define_input_reader, input)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Removes inputs and their accessor readers from `klass`.
|
|
55
|
+
#
|
|
56
|
+
# @param klass [Class]
|
|
57
|
+
# @param names [Array<Symbol>]
|
|
58
|
+
# @return [Inputs] self for chaining
|
|
59
|
+
def deregister(klass, *names)
|
|
60
|
+
names.each do |name|
|
|
61
|
+
input = registry.delete(name.to_sym)
|
|
62
|
+
klass.send(:undefine_input_reader, input)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def empty?
|
|
70
|
+
registry.empty?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [Integer]
|
|
74
|
+
def size
|
|
75
|
+
registry.size
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Resolves every input (with children) for `task`, setting each input's
|
|
79
|
+
# computed value into its backing ivar so the generated readers return it.
|
|
80
|
+
#
|
|
81
|
+
# @param task [Task]
|
|
82
|
+
# @return [void]
|
|
83
|
+
def resolve(task)
|
|
84
|
+
registry.each_value do |input|
|
|
85
|
+
value = input.resolve(task)
|
|
86
|
+
task.instance_variable_set(input.ivar_name, value)
|
|
87
|
+
resolve_children(input, value, task)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# @param input [Input] parent input whose children should be resolved
|
|
94
|
+
# @param parent_value [Object] resolved parent value child inputs read from
|
|
95
|
+
# @param task [Task]
|
|
96
|
+
# @return [void]
|
|
97
|
+
def resolve_children(input, parent_value, task)
|
|
98
|
+
return if input.children.empty? || parent_value.nil?
|
|
99
|
+
|
|
100
|
+
input.children.each do |child|
|
|
101
|
+
child_value = child.resolve_from_parent(parent_value, task)
|
|
102
|
+
task.instance_variable_set(child.ivar_name, child_value)
|
|
103
|
+
resolve_children(child, child_value, task)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# DSL receiver for the block passed to {Inputs#register}. Builds a frozen
|
|
108
|
+
# list of child {Input}s. Supports arbitrary nesting: every DSL method
|
|
109
|
+
# accepts its own block.
|
|
110
|
+
class ChildBuilder
|
|
111
|
+
|
|
112
|
+
class << self
|
|
113
|
+
|
|
114
|
+
# @yield (see Inputs#register)
|
|
115
|
+
# @return [Array<Input>] frozen list of built children
|
|
116
|
+
def build(&)
|
|
117
|
+
builder = new
|
|
118
|
+
builder.instance_eval(&)
|
|
119
|
+
builder.children.freeze
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
attr_reader :children
|
|
125
|
+
|
|
126
|
+
def initialize
|
|
127
|
+
@children = []
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @param names [Array<Symbol>]
|
|
131
|
+
# @param options [Hash{Symbol => Object}] forwarded to {Input#initialize}
|
|
132
|
+
# @option options [String] :description (also accepts `:desc`)
|
|
133
|
+
# @option options [Symbol] :as overrides the accessor name
|
|
134
|
+
# @option options [Boolean, String] :prefix prefix for the accessor name
|
|
135
|
+
# @option options [Boolean, String] :suffix suffix for the accessor name
|
|
136
|
+
# @option options [Symbol, Proc, #call] :source (`:context`) where to fetch from
|
|
137
|
+
# @option options [Object, Symbol, Proc, #call] :default
|
|
138
|
+
# @option options [Symbol, Proc, #call] :transform mutator applied after coercion
|
|
139
|
+
# @option options [Symbol, Proc, #call] :if
|
|
140
|
+
# @option options [Symbol, Proc, #call] :unless
|
|
141
|
+
# @option options [Boolean] :required
|
|
142
|
+
# @option options [Object] :coerce forwarded with declaration (see {Coercions#extract})
|
|
143
|
+
# @option options [Object] :validate forwarded with declaration (see {Validators#extract})
|
|
144
|
+
# @yield nested child input DSL
|
|
145
|
+
# @return [Array<Input>]
|
|
146
|
+
def inputs(*names, **options, &)
|
|
147
|
+
build(*names, **options, &)
|
|
148
|
+
end
|
|
149
|
+
alias input inputs
|
|
150
|
+
|
|
151
|
+
# Declares optional child inputs (equivalent to `inputs ..., required: false`).
|
|
152
|
+
# @param names [Array<Symbol>]
|
|
153
|
+
# @param options [Hash{Symbol => Object}] forwarded to {Input#initialize}
|
|
154
|
+
# @option options [String] :description (also accepts `:desc`)
|
|
155
|
+
# @option options [Symbol] :as overrides the accessor name
|
|
156
|
+
# @option options [Boolean, String] :prefix prefix for the accessor name
|
|
157
|
+
# @option options [Boolean, String] :suffix suffix for the accessor name
|
|
158
|
+
# @option options [Symbol, Proc, #call] :source (`:context`) where to fetch from
|
|
159
|
+
# @option options [Object, Symbol, Proc, #call] :default
|
|
160
|
+
# @option options [Symbol, Proc, #call] :transform mutator applied after coercion
|
|
161
|
+
# @option options [Symbol, Proc, #call] :if
|
|
162
|
+
# @option options [Symbol, Proc, #call] :unless
|
|
163
|
+
# @option options [Object] :coerce forwarded with declaration (see {Coercions#extract})
|
|
164
|
+
# @option options [Object] :validate forwarded with declaration (see {Validators#extract})
|
|
165
|
+
# @yield nested child input DSL
|
|
166
|
+
# @return [Array<Input>]
|
|
167
|
+
def optional(*names, **options, &)
|
|
168
|
+
build(*names, required: false, **options, &)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Declares required child inputs (equivalent to `inputs ..., required: true`).
|
|
172
|
+
# @param names [Array<Symbol>]
|
|
173
|
+
# @param options [Hash{Symbol => Object}] forwarded to {Input#initialize}
|
|
174
|
+
# @option options [String] :description (also accepts `:desc`)
|
|
175
|
+
# @option options [Symbol] :as overrides the accessor name
|
|
176
|
+
# @option options [Boolean, String] :prefix prefix for the accessor name
|
|
177
|
+
# @option options [Boolean, String] :suffix suffix for the accessor name
|
|
178
|
+
# @option options [Symbol, Proc, #call] :source (`:context`) where to fetch from
|
|
179
|
+
# @option options [Object, Symbol, Proc, #call] :default
|
|
180
|
+
# @option options [Symbol, Proc, #call] :transform mutator applied after coercion
|
|
181
|
+
# @option options [Symbol, Proc, #call] :if
|
|
182
|
+
# @option options [Symbol, Proc, #call] :unless
|
|
183
|
+
# @option options [Object] :coerce forwarded with declaration (see {Coercions#extract})
|
|
184
|
+
# @option options [Object] :validate forwarded with declaration (see {Validators#extract})
|
|
185
|
+
# @yield nested child input DSL
|
|
186
|
+
# @return [Array<Input>]
|
|
187
|
+
def required(*names, **options, &)
|
|
188
|
+
build(*names, required: true, **options, &)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
# @param names [Array<Symbol>]
|
|
194
|
+
# @param block [#call, nil]
|
|
195
|
+
# @param options [Hash{Symbol => Object}] forwarded to {Input#initialize}
|
|
196
|
+
# @option options [String] :description (also accepts `:desc`)
|
|
197
|
+
# @option options [Symbol] :as overrides the accessor name
|
|
198
|
+
# @option options [Boolean, String] :prefix prefix for the accessor name
|
|
199
|
+
# @option options [Boolean, String] :suffix suffix for the accessor name
|
|
200
|
+
# @option options [Symbol, Proc, #call] :source (`:context`) where to fetch from
|
|
201
|
+
# @option options [Object, Symbol, Proc, #call] :default
|
|
202
|
+
# @option options [Symbol, Proc, #call] :transform mutator applied after coercion
|
|
203
|
+
# @option options [Symbol, Proc, #call] :if
|
|
204
|
+
# @option options [Symbol, Proc, #call] :unless
|
|
205
|
+
# @option options [Boolean] :required
|
|
206
|
+
# @option options [Object] :coerce forwarded with declaration (see {Coercions#extract})
|
|
207
|
+
# @option options [Object] :validate forwarded with declaration (see {Validators#extract})
|
|
208
|
+
# @return [Array<Input>]
|
|
209
|
+
# @yield nested child input DSL
|
|
210
|
+
def build(*names, **options, &block)
|
|
211
|
+
nested = block ? self.class.build(&block) : EMPTY_ARRAY
|
|
212
|
+
names.map { |name| children << Input.new(name, children: nested, **options) }
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -2,34 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
4
|
module LogFormatters
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# including severity, timestamp, program name, process ID, and formatted message.
|
|
9
|
-
# The output is suitable for log aggregation systems and structured analysis.
|
|
5
|
+
# `Logger` formatter that emits one JSON object per line with `severity`,
|
|
6
|
+
# ISO8601 `timestamp`, `progname`, `pid`, and `message` (rendered via
|
|
7
|
+
# `#to_h` when available — Result instances serialize themselves).
|
|
10
8
|
class JSON
|
|
11
9
|
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# @param
|
|
15
|
-
# @param
|
|
16
|
-
# @
|
|
17
|
-
# @param message [Object] The log message content
|
|
18
|
-
#
|
|
19
|
-
# @return [String] A JSON-formatted log entry with a trailing newline
|
|
20
|
-
#
|
|
21
|
-
# @example Basic usage
|
|
22
|
-
# logger_formatter.call("INFO", Time.now, "MyApp", "User logged in")
|
|
23
|
-
# # => '{"severity":"INFO","timestamp":"2024-01-15T10:30:45.123456Z","progname":"MyApp","pid":12345,"message":"User logged in"}\n'
|
|
24
|
-
#
|
|
25
|
-
# @rbs (String severity, Time time, String? progname, String message) -> String
|
|
10
|
+
# @param severity [String] Logger severity name
|
|
11
|
+
# @param time [Time]
|
|
12
|
+
# @param progname [String, nil]
|
|
13
|
+
# @param message [Object] anything `Logger` was handed
|
|
14
|
+
# @return [String] JSON line terminated by `"\n"`
|
|
26
15
|
def call(severity, time, progname, message)
|
|
27
16
|
hash = {
|
|
28
17
|
severity:,
|
|
29
18
|
timestamp: time.utc.iso8601(6),
|
|
30
19
|
progname:,
|
|
31
20
|
pid: Process.pid,
|
|
32
|
-
message:
|
|
21
|
+
message: message.respond_to?(:to_h) ? message.to_h : message
|
|
33
22
|
}
|
|
34
23
|
|
|
35
24
|
::JSON.dump(hash) << "\n"
|
|
@@ -2,37 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
4
|
module LogFormatters
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# including severity, timestamp, program name, process ID, and formatted message.
|
|
9
|
-
# The output is suitable for log parsing tools and human-readable structured logs.
|
|
5
|
+
# `Logger` formatter that emits `key=value.inspect` pairs on a single
|
|
6
|
+
# line. Hash-like messages (including Result) are flattened into the
|
|
7
|
+
# top-level `message` field via `#to_h`.
|
|
10
8
|
class KeyValue
|
|
11
9
|
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# @param
|
|
15
|
-
# @param
|
|
16
|
-
# @
|
|
17
|
-
# @param message [Object] The log message content
|
|
18
|
-
#
|
|
19
|
-
# @return [String] A key-value formatted log entry with a trailing newline
|
|
20
|
-
#
|
|
21
|
-
# @example Basic usage
|
|
22
|
-
# logger_formatter.call("INFO", Time.now, "MyApp", "User logged in")
|
|
23
|
-
# # => "severity=INFO timestamp=2024-01-15T10:30:45.123456Z progname=MyApp pid=12345 message=User logged in\n"
|
|
24
|
-
#
|
|
25
|
-
# @rbs (String severity, Time time, String? progname, String message) -> String
|
|
10
|
+
# @param severity [String] Logger severity name
|
|
11
|
+
# @param time [Time]
|
|
12
|
+
# @param progname [String, nil]
|
|
13
|
+
# @param message [Object]
|
|
14
|
+
# @return [String] single-line key=value line terminated by `"\n"`
|
|
26
15
|
def call(severity, time, progname, message)
|
|
27
16
|
hash = {
|
|
28
17
|
severity:,
|
|
29
18
|
timestamp: time.utc.iso8601(6),
|
|
30
19
|
progname:,
|
|
31
20
|
pid: Process.pid,
|
|
32
|
-
message:
|
|
21
|
+
message: message.respond_to?(:to_h) ? message.to_h : message
|
|
33
22
|
}
|
|
34
23
|
|
|
35
|
-
|
|
24
|
+
hash.map { |k, v| "#{k}=#{v.inspect}" }.join(" ") << "\n"
|
|
36
25
|
end
|
|
37
26
|
|
|
38
27
|
end
|
|
@@ -2,27 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
4
|
module LogFormatters
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# This formatter converts log entries into a compact single-line format with
|
|
8
|
-
# severity abbreviation, ISO8601 timestamp, process ID, and formatted message.
|
|
9
|
-
# The output is optimized for human readability and traditional log file formats.
|
|
5
|
+
# Default formatter. Emits a human-readable single-line log entry that
|
|
6
|
+
# mirrors Ruby's built-in `Logger::Formatter` style.
|
|
10
7
|
class Line
|
|
11
8
|
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# @param
|
|
15
|
-
# @param
|
|
16
|
-
# @
|
|
17
|
-
# @param message [Object] The log message content
|
|
18
|
-
#
|
|
19
|
-
# @return [String] A single-line formatted log entry with a trailing newline
|
|
20
|
-
#
|
|
21
|
-
# @example Basic usage
|
|
22
|
-
# logger_formatter.call("INFO", Time.now, "MyApp", "User logged in")
|
|
23
|
-
# # => "I, [2024-01-15T10:30:45.123456Z #12345] INFO -- MyApp: User logged in\n"
|
|
24
|
-
#
|
|
25
|
-
# @rbs (String severity, Time time, String? progname, String message) -> String
|
|
9
|
+
# @param severity [String] Logger severity name
|
|
10
|
+
# @param time [Time]
|
|
11
|
+
# @param progname [String, nil]
|
|
12
|
+
# @param message [Object]
|
|
13
|
+
# @return [String] formatted line terminated by `"\n"`
|
|
26
14
|
def call(severity, time, progname, message)
|
|
27
15
|
"#{severity[0]}, [#{time.utc.iso8601(6)} ##{Process.pid}] #{severity} -- #{progname}: #{message}\n"
|
|
28
16
|
end
|
|
@@ -2,34 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
4
|
module LogFormatters
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# This formatter converts log entries into Logstash-compatible JSON format with
|
|
8
|
-
# standardized fields including @version, @timestamp, severity, program name,
|
|
9
|
-
# process ID, and formatted message. The output follows Logstash event format
|
|
10
|
-
# specifications for seamless integration with ELK stack and similar systems.
|
|
5
|
+
# `Logger` formatter that produces one JSON line per entry in the shape
|
|
6
|
+
# expected by Logstash (`@version` + `@timestamp`).
|
|
11
7
|
class Logstash
|
|
12
8
|
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# @param
|
|
16
|
-
# @param
|
|
17
|
-
# @
|
|
18
|
-
# @param message [Object] The log message content
|
|
19
|
-
#
|
|
20
|
-
# @return [String] A Logstash-compatible JSON-formatted log entry with a trailing newline
|
|
21
|
-
#
|
|
22
|
-
# @example Basic usage
|
|
23
|
-
# logger_formatter.call("INFO", Time.now, "MyApp", "User logged in")
|
|
24
|
-
# # => '{"severity":"INFO","progname":"MyApp","pid":12345,"message":"User logged in","@version":"1","@timestamp":"2024-01-15T10:30:45.123456Z"}\n'
|
|
25
|
-
#
|
|
26
|
-
# @rbs (String severity, Time time, String? progname, String message) -> String
|
|
9
|
+
# @param severity [String] Logger severity name
|
|
10
|
+
# @param time [Time]
|
|
11
|
+
# @param progname [String, nil]
|
|
12
|
+
# @param message [Object]
|
|
13
|
+
# @return [String] JSON line terminated by `"\n"`
|
|
27
14
|
def call(severity, time, progname, message)
|
|
28
15
|
hash = {
|
|
29
16
|
severity:,
|
|
30
17
|
progname:,
|
|
31
18
|
pid: Process.pid,
|
|
32
|
-
message:
|
|
19
|
+
message: message.respond_to?(:to_h) ? message.to_h : message,
|
|
33
20
|
"@version" => "1",
|
|
34
21
|
"@timestamp" => time.utc.iso8601(6)
|
|
35
22
|
}
|
|
@@ -2,28 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
4
|
module LogFormatters
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# processing, adding only a trailing newline. It's useful for scenarios
|
|
9
|
-
# where you want to preserve the exact message content without metadata
|
|
10
|
-
# or structured formatting.
|
|
5
|
+
# Passthrough formatter that writes only the message (terminated with
|
|
6
|
+
# `"\n"`). Useful when surrounding infrastructure already supplies
|
|
7
|
+
# severity and timestamp.
|
|
11
8
|
class Raw
|
|
12
9
|
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# @param
|
|
16
|
-
# @param
|
|
17
|
-
# @
|
|
18
|
-
# @param message [Object] The log message content
|
|
19
|
-
#
|
|
20
|
-
# @return [String] The raw message with a trailing newline
|
|
21
|
-
#
|
|
22
|
-
# @example Basic usage
|
|
23
|
-
# logger_formatter.call("INFO", Time.now, "MyApp", "User logged in")
|
|
24
|
-
# # => "User logged in\n"
|
|
25
|
-
#
|
|
26
|
-
# @rbs (String severity, Time time, String? progname, String message) -> String
|
|
10
|
+
# @param severity [String] ignored
|
|
11
|
+
# @param time [Time] ignored
|
|
12
|
+
# @param progname [String, nil] ignored
|
|
13
|
+
# @param message [Object]
|
|
14
|
+
# @return [String] `"#{message}\n"`
|
|
27
15
|
def call(severity, time, progname, message)
|
|
28
16
|
"#{message}\n"
|
|
29
17
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
# Returns a logger tailored to a task's settings. If the task overrides
|
|
5
|
+
# `log_level` or `log_formatter`, the base logger is `dup`'d so those
|
|
6
|
+
# overrides don't leak into sibling tasks sharing the same global logger.
|
|
7
|
+
module LoggerProxy
|
|
8
|
+
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
# @param task [Task]
|
|
12
|
+
# @return [Logger] a logger configured with the task's level/formatter
|
|
13
|
+
def logger(task)
|
|
14
|
+
settings = task.class.settings
|
|
15
|
+
logger = settings.logger
|
|
16
|
+
level = settings.log_level
|
|
17
|
+
formatter = settings.log_formatter
|
|
18
|
+
|
|
19
|
+
change_level = level && level != logger.level
|
|
20
|
+
change_formatter = formatter && !logger.formatter.equal?(formatter)
|
|
21
|
+
return logger unless change_level || change_formatter
|
|
22
|
+
|
|
23
|
+
logger = logger.dup
|
|
24
|
+
logger.level = level if change_level
|
|
25
|
+
logger.formatter = formatter if change_formatter
|
|
26
|
+
logger
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
class Mergers
|
|
5
|
+
# Recursively merges `Hash` values from the parallel task's context into
|
|
6
|
+
# the workflow context. Scalar-vs-hash collisions still follow
|
|
7
|
+
# last-write-wins.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
module DeepMerge
|
|
11
|
+
|
|
12
|
+
extend self
|
|
13
|
+
|
|
14
|
+
# @param ctx [Context] workflow context being folded into
|
|
15
|
+
# @param result [Result] successful parallel task result
|
|
16
|
+
# @return [void]
|
|
17
|
+
def call(ctx, result)
|
|
18
|
+
ctx.deep_merge(result.context)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
class Mergers
|
|
5
|
+
# Default merger. Shallow-merges the parallel task's context into the
|
|
6
|
+
# workflow context via `Hash#merge` semantics; on key conflicts, the
|
|
7
|
+
# later-declared task wins.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
module LastWriteWins
|
|
11
|
+
|
|
12
|
+
extend self
|
|
13
|
+
|
|
14
|
+
# @param ctx [Context] workflow context being folded into
|
|
15
|
+
# @param result [Result] successful parallel task result
|
|
16
|
+
# @return [void]
|
|
17
|
+
def call(ctx, result)
|
|
18
|
+
ctx.merge(result.context)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
class Mergers
|
|
5
|
+
# No-op merger. Leaves the workflow context untouched; per-task results
|
|
6
|
+
# remain inspectable via `result.chain`.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
module NoMerge
|
|
10
|
+
|
|
11
|
+
extend self
|
|
12
|
+
|
|
13
|
+
# @param _ctx [Context] ignored
|
|
14
|
+
# @param _result [Result] ignored
|
|
15
|
+
# @return [void]
|
|
16
|
+
def call(_ctx, _result); end
|
|
17
|
+
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/cmdx/mergers.rb
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
# Registry of named merge strategies used to fold successful parallel task
|
|
5
|
+
# results back into the workflow context. Ships with built-ins for
|
|
6
|
+
# `:last_write_wins` (default), `:deep_merge`, and `:no_merge`. A merger is
|
|
7
|
+
# any callable accepting `call(workflow_context, result)`.
|
|
8
|
+
class Mergers
|
|
9
|
+
|
|
10
|
+
attr_reader :registry
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@registry = {
|
|
14
|
+
last_write_wins: Mergers::LastWriteWins,
|
|
15
|
+
deep_merge: Mergers::DeepMerge,
|
|
16
|
+
no_merge: Mergers::NoMerge
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param source [Mergers] registry to duplicate
|
|
21
|
+
# @return [void]
|
|
22
|
+
def initialize_copy(source)
|
|
23
|
+
@registry = source.registry.dup
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Registers a named merger, 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] merger callable when `callable` is omitted
|
|
31
|
+
# @yield merger body — `call(workflow_context, result)`
|
|
32
|
+
# @return [Mergers] self for chaining
|
|
33
|
+
# @raise [ArgumentError] when both `callable` and a block are given, or
|
|
34
|
+
# when the resolved merger isn't callable
|
|
35
|
+
def register(name, callable = nil, &block)
|
|
36
|
+
merger = callable || block
|
|
37
|
+
|
|
38
|
+
if callable && block
|
|
39
|
+
raise ArgumentError, "provide either a callable or a block, not both"
|
|
40
|
+
elsif !merger.respond_to?(:call)
|
|
41
|
+
raise ArgumentError, "merger must respond to #call"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
registry[name.to_sym] = merger
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param name [Symbol]
|
|
49
|
+
# @return [Mergers] 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 merger
|
|
57
|
+
# @raise [ArgumentError] when `name` isn't registered
|
|
58
|
+
def lookup(name)
|
|
59
|
+
registry[name] || begin
|
|
60
|
+
raise ArgumentError, "unknown merger: #{name.inspect}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Resolves a declaration's `:merger` option to a concrete
|
|
65
|
+
# callable. Accepts `nil` (default `:last_write_wins`), a Symbol
|
|
66
|
+
# (registry lookup), or any 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(:last_write_wins)
|
|
75
|
+
when Symbol
|
|
76
|
+
lookup(spec)
|
|
77
|
+
else
|
|
78
|
+
return spec if spec.respond_to?(:call)
|
|
79
|
+
|
|
80
|
+
raise ArgumentError, "unknown merger: #{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
|