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.
Files changed (195) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +131 -1
  3. data/README.md +37 -24
  4. data/lib/cmdx/.DS_Store +0 -0
  5. data/lib/cmdx/callbacks.rb +179 -0
  6. data/lib/cmdx/chain.rb +78 -175
  7. data/lib/cmdx/coercions/array.rb +19 -33
  8. data/lib/cmdx/coercions/big_decimal.rb +12 -29
  9. data/lib/cmdx/coercions/boolean.rb +25 -45
  10. data/lib/cmdx/coercions/coerce.rb +32 -0
  11. data/lib/cmdx/coercions/complex.rb +12 -27
  12. data/lib/cmdx/coercions/date.rb +29 -33
  13. data/lib/cmdx/coercions/date_time.rb +29 -33
  14. data/lib/cmdx/coercions/float.rb +8 -29
  15. data/lib/cmdx/coercions/hash.rb +17 -43
  16. data/lib/cmdx/coercions/integer.rb +8 -32
  17. data/lib/cmdx/coercions/rational.rb +12 -33
  18. data/lib/cmdx/coercions/string.rb +6 -24
  19. data/lib/cmdx/coercions/symbol.rb +12 -26
  20. data/lib/cmdx/coercions/time.rb +31 -35
  21. data/lib/cmdx/coercions.rb +174 -0
  22. data/lib/cmdx/configuration.rb +45 -225
  23. data/lib/cmdx/context.rb +263 -242
  24. data/lib/cmdx/deprecation.rb +67 -0
  25. data/lib/cmdx/deprecators/error.rb +22 -0
  26. data/lib/cmdx/deprecators/log.rb +22 -0
  27. data/lib/cmdx/deprecators/warn.rb +21 -0
  28. data/lib/cmdx/deprecators.rb +101 -0
  29. data/lib/cmdx/errors.rb +145 -79
  30. data/lib/cmdx/executors/fiber.rb +42 -0
  31. data/lib/cmdx/executors/thread.rb +36 -0
  32. data/lib/cmdx/executors.rb +95 -0
  33. data/lib/cmdx/fault.rb +85 -78
  34. data/lib/cmdx/i18n_proxy.rb +104 -0
  35. data/lib/cmdx/input.rb +294 -0
  36. data/lib/cmdx/inputs.rb +218 -0
  37. data/lib/cmdx/log_formatters/json.rb +9 -20
  38. data/lib/cmdx/log_formatters/key_value.rb +10 -21
  39. data/lib/cmdx/log_formatters/line.rb +7 -19
  40. data/lib/cmdx/log_formatters/logstash.rb +8 -21
  41. data/lib/cmdx/log_formatters/raw.rb +8 -20
  42. data/lib/cmdx/logger_proxy.rb +30 -0
  43. data/lib/cmdx/mergers/deep_merge.rb +23 -0
  44. data/lib/cmdx/mergers/last_write_wins.rb +23 -0
  45. data/lib/cmdx/mergers/no_merge.rb +20 -0
  46. data/lib/cmdx/mergers.rb +95 -0
  47. data/lib/cmdx/middlewares.rb +128 -0
  48. data/lib/cmdx/output.rb +115 -0
  49. data/lib/cmdx/outputs.rb +66 -0
  50. data/lib/cmdx/pipeline.rb +144 -131
  51. data/lib/cmdx/railtie.rb +10 -36
  52. data/lib/cmdx/result.rb +252 -473
  53. data/lib/cmdx/retriers/bounded_random.rb +24 -0
  54. data/lib/cmdx/retriers/decorrelated_jitter.rb +28 -0
  55. data/lib/cmdx/retriers/exponential.rb +23 -0
  56. data/lib/cmdx/retriers/fibonacci.rb +39 -0
  57. data/lib/cmdx/retriers/full_random.rb +23 -0
  58. data/lib/cmdx/retriers/half_random.rb +24 -0
  59. data/lib/cmdx/retriers/linear.rb +23 -0
  60. data/lib/cmdx/retriers.rb +106 -0
  61. data/lib/cmdx/retry.rb +117 -138
  62. data/lib/cmdx/runtime.rb +251 -0
  63. data/lib/cmdx/settings.rb +68 -196
  64. data/lib/cmdx/signal.rb +165 -0
  65. data/lib/cmdx/task.rb +443 -336
  66. data/lib/cmdx/telemetry.rb +108 -0
  67. data/lib/cmdx/util.rb +73 -0
  68. data/lib/cmdx/validators/absence.rb +10 -39
  69. data/lib/cmdx/validators/exclusion.rb +33 -52
  70. data/lib/cmdx/validators/format.rb +19 -49
  71. data/lib/cmdx/validators/inclusion.rb +33 -54
  72. data/lib/cmdx/validators/length.rb +125 -127
  73. data/lib/cmdx/validators/numeric.rb +123 -123
  74. data/lib/cmdx/validators/presence.rb +10 -39
  75. data/lib/cmdx/validators/validate.rb +31 -0
  76. data/lib/cmdx/validators.rb +161 -0
  77. data/lib/cmdx/version.rb +2 -4
  78. data/lib/cmdx/workflow.rb +74 -82
  79. data/lib/cmdx.rb +111 -42
  80. data/lib/generators/cmdx/install_generator.rb +7 -17
  81. data/lib/generators/cmdx/task_generator.rb +12 -29
  82. data/lib/generators/cmdx/templates/install.rb +128 -52
  83. data/lib/generators/cmdx/templates/task.rb.tt +1 -1
  84. data/lib/generators/cmdx/templates/workflow.rb.tt +1 -2
  85. data/lib/generators/cmdx/workflow_generator.rb +12 -29
  86. data/lib/locales/en.yml +9 -6
  87. data/mkdocs.yml +25 -23
  88. metadata +39 -138
  89. data/lib/cmdx/attribute.rb +0 -440
  90. data/lib/cmdx/attribute_registry.rb +0 -185
  91. data/lib/cmdx/attribute_value.rb +0 -252
  92. data/lib/cmdx/callback_registry.rb +0 -169
  93. data/lib/cmdx/coercion_registry.rb +0 -138
  94. data/lib/cmdx/deprecator.rb +0 -77
  95. data/lib/cmdx/exception.rb +0 -46
  96. data/lib/cmdx/executor.rb +0 -374
  97. data/lib/cmdx/identifier.rb +0 -30
  98. data/lib/cmdx/locale.rb +0 -78
  99. data/lib/cmdx/middleware_registry.rb +0 -148
  100. data/lib/cmdx/middlewares/correlate.rb +0 -140
  101. data/lib/cmdx/middlewares/runtime.rb +0 -62
  102. data/lib/cmdx/middlewares/timeout.rb +0 -78
  103. data/lib/cmdx/parallelizer.rb +0 -100
  104. data/lib/cmdx/utils/call.rb +0 -53
  105. data/lib/cmdx/utils/condition.rb +0 -71
  106. data/lib/cmdx/utils/format.rb +0 -82
  107. data/lib/cmdx/utils/normalize.rb +0 -52
  108. data/lib/cmdx/utils/wrap.rb +0 -38
  109. data/lib/cmdx/validator_registry.rb +0 -143
  110. data/lib/generators/cmdx/locale_generator.rb +0 -39
  111. data/lib/locales/af.yml +0 -53
  112. data/lib/locales/ar.yml +0 -53
  113. data/lib/locales/az.yml +0 -53
  114. data/lib/locales/be.yml +0 -53
  115. data/lib/locales/bg.yml +0 -53
  116. data/lib/locales/bn.yml +0 -53
  117. data/lib/locales/bs.yml +0 -53
  118. data/lib/locales/ca.yml +0 -53
  119. data/lib/locales/cnr.yml +0 -53
  120. data/lib/locales/cs.yml +0 -53
  121. data/lib/locales/cy.yml +0 -53
  122. data/lib/locales/da.yml +0 -53
  123. data/lib/locales/de.yml +0 -53
  124. data/lib/locales/dz.yml +0 -53
  125. data/lib/locales/el.yml +0 -53
  126. data/lib/locales/eo.yml +0 -53
  127. data/lib/locales/es.yml +0 -53
  128. data/lib/locales/et.yml +0 -53
  129. data/lib/locales/eu.yml +0 -53
  130. data/lib/locales/fa.yml +0 -53
  131. data/lib/locales/fi.yml +0 -53
  132. data/lib/locales/fr.yml +0 -53
  133. data/lib/locales/fy.yml +0 -53
  134. data/lib/locales/gd.yml +0 -53
  135. data/lib/locales/gl.yml +0 -53
  136. data/lib/locales/he.yml +0 -53
  137. data/lib/locales/hi.yml +0 -53
  138. data/lib/locales/hr.yml +0 -53
  139. data/lib/locales/hu.yml +0 -53
  140. data/lib/locales/hy.yml +0 -53
  141. data/lib/locales/id.yml +0 -53
  142. data/lib/locales/is.yml +0 -53
  143. data/lib/locales/it.yml +0 -53
  144. data/lib/locales/ja.yml +0 -53
  145. data/lib/locales/ka.yml +0 -53
  146. data/lib/locales/kk.yml +0 -53
  147. data/lib/locales/km.yml +0 -53
  148. data/lib/locales/kn.yml +0 -53
  149. data/lib/locales/ko.yml +0 -53
  150. data/lib/locales/lb.yml +0 -53
  151. data/lib/locales/lo.yml +0 -53
  152. data/lib/locales/lt.yml +0 -53
  153. data/lib/locales/lv.yml +0 -53
  154. data/lib/locales/mg.yml +0 -53
  155. data/lib/locales/mk.yml +0 -53
  156. data/lib/locales/ml.yml +0 -53
  157. data/lib/locales/mn.yml +0 -53
  158. data/lib/locales/mr-IN.yml +0 -53
  159. data/lib/locales/ms.yml +0 -53
  160. data/lib/locales/nb.yml +0 -53
  161. data/lib/locales/ne.yml +0 -53
  162. data/lib/locales/nl.yml +0 -53
  163. data/lib/locales/nn.yml +0 -53
  164. data/lib/locales/oc.yml +0 -53
  165. data/lib/locales/or.yml +0 -53
  166. data/lib/locales/pa.yml +0 -53
  167. data/lib/locales/pl.yml +0 -53
  168. data/lib/locales/pt.yml +0 -53
  169. data/lib/locales/rm.yml +0 -53
  170. data/lib/locales/ro.yml +0 -53
  171. data/lib/locales/ru.yml +0 -53
  172. data/lib/locales/sc.yml +0 -53
  173. data/lib/locales/sk.yml +0 -53
  174. data/lib/locales/sl.yml +0 -53
  175. data/lib/locales/sq.yml +0 -53
  176. data/lib/locales/sr.yml +0 -53
  177. data/lib/locales/st.yml +0 -53
  178. data/lib/locales/sv.yml +0 -53
  179. data/lib/locales/sw.yml +0 -53
  180. data/lib/locales/ta.yml +0 -53
  181. data/lib/locales/te.yml +0 -53
  182. data/lib/locales/th.yml +0 -53
  183. data/lib/locales/tl.yml +0 -53
  184. data/lib/locales/tr.yml +0 -53
  185. data/lib/locales/tt.yml +0 -53
  186. data/lib/locales/ug.yml +0 -53
  187. data/lib/locales/uk.yml +0 -53
  188. data/lib/locales/ur.yml +0 -53
  189. data/lib/locales/uz.yml +0 -53
  190. data/lib/locales/vi.yml +0 -53
  191. data/lib/locales/wo.yml +0 -53
  192. data/lib/locales/zh-CN.yml +0 -53
  193. data/lib/locales/zh-HK.yml +0 -53
  194. data/lib/locales/zh-TW.yml +0 -53
  195. data/lib/locales/zh-YUE.yml +0 -53
@@ -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
- # Formats log messages as JSON for structured logging
6
- #
7
- # This formatter converts log entries into JSON format with standardized fields
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
- # Formats a log entry as a JSON string
13
- #
14
- # @param severity [String] The log level (e.g., "INFO", "ERROR", "DEBUG")
15
- # @param time [Time] The timestamp when the log entry was created
16
- # @param progname [String, nil] The program name or identifier
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: Utils::Format.to_log(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
- # Formats log messages as key-value pairs for structured logging
6
- #
7
- # This formatter converts log entries into key-value format with standardized fields
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
- # Formats a log entry as a key-value string
13
- #
14
- # @param severity [String] The log level (e.g., "INFO", "ERROR", "DEBUG")
15
- # @param time [Time] The timestamp when the log entry was created
16
- # @param progname [String, nil] The program name or identifier
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: Utils::Format.to_log(message)
21
+ message: message.respond_to?(:to_h) ? message.to_h : message
33
22
  }
34
23
 
35
- Utils::Format.to_str(hash) << "\n"
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
- # Formats log messages as single-line text for human-readable logging
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
- # Formats a log entry as a single-line string
13
- #
14
- # @param severity [String] The log level (e.g., "INFO", "ERROR", "DEBUG")
15
- # @param time [Time] The timestamp when the log entry was created
16
- # @param progname [String, nil] The program name or identifier
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
- # Formats log messages as Logstash-compatible JSON for structured logging
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
- # Formats a log entry as a Logstash-compatible JSON string
14
- #
15
- # @param severity [String] The log level (e.g., "INFO", "ERROR", "DEBUG")
16
- # @param time [Time] The timestamp when the log entry was created
17
- # @param progname [String, nil] The program name or identifier
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: Utils::Format.to_log(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
- # Formats log messages as raw text without additional formatting
6
- #
7
- # This formatter outputs log messages in their original form with minimal
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
- # Formats a log entry as raw text
14
- #
15
- # @param severity [String] The log level (e.g., "INFO", "ERROR", "DEBUG")
16
- # @param time [Time] The timestamp when the log entry was created
17
- # @param progname [String, nil] The program name or identifier
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
@@ -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