cmdx 1.19.0 → 1.21.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/.DS_Store +0 -0
- data/CHANGELOG.md +82 -16
- data/README.md +1 -1
- data/lib/cmdx/attribute.rb +82 -19
- data/lib/cmdx/attribute_registry.rb +79 -8
- data/lib/cmdx/attribute_value.rb +2 -2
- data/lib/cmdx/callback_registry.rb +60 -26
- data/lib/cmdx/chain.rb +34 -5
- data/lib/cmdx/coercion_registry.rb +42 -20
- data/lib/cmdx/coercions/array.rb +2 -2
- data/lib/cmdx/coercions/big_decimal.rb +1 -1
- data/lib/cmdx/coercions/boolean.rb +2 -2
- data/lib/cmdx/coercions/complex.rb +1 -1
- data/lib/cmdx/coercions/date.rb +1 -1
- data/lib/cmdx/coercions/date_time.rb +1 -1
- data/lib/cmdx/coercions/float.rb +1 -1
- data/lib/cmdx/coercions/hash.rb +1 -1
- data/lib/cmdx/coercions/integer.rb +1 -1
- data/lib/cmdx/coercions/rational.rb +1 -1
- data/lib/cmdx/coercions/string.rb +1 -1
- data/lib/cmdx/coercions/symbol.rb +1 -1
- data/lib/cmdx/coercions/time.rb +1 -1
- data/lib/cmdx/configuration.rb +38 -0
- data/lib/cmdx/context.rb +11 -8
- data/lib/cmdx/deprecator.rb +27 -14
- data/lib/cmdx/errors.rb +3 -4
- data/lib/cmdx/exception.rb +7 -0
- data/lib/cmdx/executor.rb +80 -53
- data/lib/cmdx/identifier.rb +4 -6
- data/lib/cmdx/locale.rb +32 -9
- data/lib/cmdx/middleware_registry.rb +43 -23
- data/lib/cmdx/middlewares/correlate.rb +4 -2
- data/lib/cmdx/middlewares/runtime.rb +18 -3
- data/lib/cmdx/middlewares/timeout.rb +11 -10
- data/lib/cmdx/parallelizer.rb +100 -0
- data/lib/cmdx/pipeline.rb +42 -23
- data/lib/cmdx/railtie.rb +1 -1
- data/lib/cmdx/result.rb +91 -19
- data/lib/cmdx/retry.rb +166 -0
- data/lib/cmdx/settings.rb +226 -0
- data/lib/cmdx/task.rb +62 -65
- data/lib/cmdx/utils/format.rb +17 -1
- data/lib/cmdx/utils/normalize.rb +52 -0
- data/lib/cmdx/utils/wrap.rb +38 -0
- data/lib/cmdx/validator_registry.rb +44 -19
- data/lib/cmdx/validators/absence.rb +1 -1
- data/lib/cmdx/validators/exclusion.rb +2 -2
- data/lib/cmdx/validators/format.rb +1 -1
- data/lib/cmdx/validators/inclusion.rb +2 -2
- data/lib/cmdx/validators/length.rb +1 -1
- data/lib/cmdx/validators/numeric.rb +1 -1
- data/lib/cmdx/validators/presence.rb +1 -1
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/workflow.rb +17 -0
- data/lib/cmdx.rb +12 -0
- data/lib/generators/cmdx/templates/install.rb +20 -5
- data/lib/locales/af.yml +2 -0
- data/lib/locales/ar.yml +2 -0
- data/lib/locales/az.yml +2 -0
- data/lib/locales/be.yml +2 -0
- data/lib/locales/bg.yml +2 -0
- data/lib/locales/bn.yml +2 -0
- data/lib/locales/bs.yml +2 -0
- data/lib/locales/ca.yml +2 -0
- data/lib/locales/cnr.yml +2 -0
- data/lib/locales/cs.yml +2 -0
- data/lib/locales/cy.yml +2 -0
- data/lib/locales/da.yml +2 -0
- data/lib/locales/de.yml +2 -0
- data/lib/locales/dz.yml +2 -0
- data/lib/locales/el.yml +2 -0
- data/lib/locales/en.yml +2 -0
- data/lib/locales/eo.yml +2 -0
- data/lib/locales/es.yml +2 -0
- data/lib/locales/et.yml +2 -0
- data/lib/locales/eu.yml +2 -0
- data/lib/locales/fa.yml +2 -0
- data/lib/locales/fi.yml +2 -0
- data/lib/locales/fr.yml +2 -0
- data/lib/locales/fy.yml +2 -0
- data/lib/locales/gd.yml +2 -0
- data/lib/locales/gl.yml +2 -0
- data/lib/locales/he.yml +2 -0
- data/lib/locales/hi.yml +2 -0
- data/lib/locales/hr.yml +2 -0
- data/lib/locales/hu.yml +2 -0
- data/lib/locales/hy.yml +2 -0
- data/lib/locales/id.yml +2 -0
- data/lib/locales/is.yml +2 -0
- data/lib/locales/it.yml +2 -0
- data/lib/locales/ja.yml +2 -0
- data/lib/locales/ka.yml +2 -0
- data/lib/locales/kk.yml +2 -0
- data/lib/locales/km.yml +2 -0
- data/lib/locales/kn.yml +2 -0
- data/lib/locales/ko.yml +2 -0
- data/lib/locales/lb.yml +2 -0
- data/lib/locales/lo.yml +2 -0
- data/lib/locales/lt.yml +2 -0
- data/lib/locales/lv.yml +2 -0
- data/lib/locales/mg.yml +2 -0
- data/lib/locales/mk.yml +2 -0
- data/lib/locales/ml.yml +2 -0
- data/lib/locales/mn.yml +2 -0
- data/lib/locales/mr-IN.yml +2 -0
- data/lib/locales/ms.yml +2 -0
- data/lib/locales/nb.yml +2 -0
- data/lib/locales/ne.yml +2 -0
- data/lib/locales/nl.yml +2 -0
- data/lib/locales/nn.yml +2 -0
- data/lib/locales/oc.yml +2 -0
- data/lib/locales/or.yml +2 -0
- data/lib/locales/pa.yml +2 -0
- data/lib/locales/pl.yml +2 -0
- data/lib/locales/pt.yml +2 -0
- data/lib/locales/rm.yml +2 -0
- data/lib/locales/ro.yml +2 -0
- data/lib/locales/ru.yml +2 -0
- data/lib/locales/sc.yml +2 -0
- data/lib/locales/sk.yml +2 -0
- data/lib/locales/sl.yml +2 -0
- data/lib/locales/sq.yml +2 -0
- data/lib/locales/sr.yml +2 -0
- data/lib/locales/st.yml +2 -0
- data/lib/locales/sv.yml +2 -0
- data/lib/locales/sw.yml +2 -0
- data/lib/locales/ta.yml +2 -0
- data/lib/locales/te.yml +2 -0
- data/lib/locales/th.yml +2 -0
- data/lib/locales/tl.yml +2 -0
- data/lib/locales/tr.yml +2 -0
- data/lib/locales/tt.yml +2 -0
- data/lib/locales/ug.yml +2 -0
- data/lib/locales/uk.yml +2 -0
- data/lib/locales/ur.yml +2 -0
- data/lib/locales/uz.yml +2 -0
- data/lib/locales/vi.yml +2 -0
- data/lib/locales/wo.yml +2 -0
- data/lib/locales/zh-CN.yml +2 -0
- data/lib/locales/zh-HK.yml +2 -0
- data/lib/locales/zh-TW.yml +2 -0
- data/lib/locales/zh-YUE.yml +2 -0
- data/mkdocs.yml +5 -1
- metadata +6 -15
data/lib/cmdx/executor.rb
CHANGED
|
@@ -10,6 +10,14 @@ module CMDx
|
|
|
10
10
|
|
|
11
11
|
extend Forwardable
|
|
12
12
|
|
|
13
|
+
# @rbs STATE_CALLBACKS: Hash[String, Symbol]
|
|
14
|
+
STATE_CALLBACKS = Result::STATES.to_h { |s| [s, :"on_#{s}"] }.freeze
|
|
15
|
+
private_constant :STATE_CALLBACKS
|
|
16
|
+
|
|
17
|
+
# @rbs STATUS_CALLBACKS: Hash[String, Symbol]
|
|
18
|
+
STATUS_CALLBACKS = Result::STATUSES.to_h { |s| [s, :"on_#{s}"] }.freeze
|
|
19
|
+
private_constant :STATUS_CALLBACKS
|
|
20
|
+
|
|
13
21
|
# Returns the task being executed.
|
|
14
22
|
#
|
|
15
23
|
# @return [Task] The task instance
|
|
@@ -63,23 +71,24 @@ module CMDx
|
|
|
63
71
|
#
|
|
64
72
|
# @rbs () -> Result
|
|
65
73
|
def execute
|
|
66
|
-
task.class.settings
|
|
74
|
+
task.class.settings.middlewares.call!(task) do
|
|
67
75
|
pre_execution! unless @pre_execution
|
|
68
76
|
execution!
|
|
69
|
-
|
|
77
|
+
verify_context_returns!
|
|
70
78
|
rescue UndefinedMethodError => e
|
|
71
|
-
|
|
79
|
+
raise_exception(e)
|
|
72
80
|
rescue Fault => e
|
|
73
81
|
result.throw!(e.result, halt: false, cause: e)
|
|
74
82
|
rescue StandardError => e
|
|
75
83
|
retry if retry_execution?(e)
|
|
76
|
-
result.fail!(
|
|
77
|
-
task.class.settings
|
|
84
|
+
result.fail!(Utils::Normalize.exception(e), halt: false, cause: e, source: :exception)
|
|
85
|
+
task.class.settings.exception_handler&.call(task, e)
|
|
78
86
|
ensure
|
|
79
87
|
result.executed!
|
|
80
88
|
post_execution!
|
|
81
89
|
end
|
|
82
90
|
|
|
91
|
+
verify_middleware_yield!
|
|
83
92
|
finalize_execution!
|
|
84
93
|
end
|
|
85
94
|
|
|
@@ -95,30 +104,38 @@ module CMDx
|
|
|
95
104
|
#
|
|
96
105
|
# @rbs () -> Result
|
|
97
106
|
def execute!
|
|
98
|
-
task.class.settings
|
|
107
|
+
task.class.settings.middlewares.call!(task) do
|
|
99
108
|
pre_execution! unless @pre_execution
|
|
100
109
|
execution!
|
|
101
|
-
|
|
110
|
+
verify_context_returns!
|
|
102
111
|
rescue UndefinedMethodError => e
|
|
103
112
|
raise_exception(e)
|
|
104
113
|
rescue Fault => e
|
|
105
114
|
result.throw!(e.result, halt: false, cause: e)
|
|
106
|
-
|
|
115
|
+
|
|
116
|
+
if halt_execution?(e)
|
|
117
|
+
raise_exception(e)
|
|
118
|
+
else
|
|
119
|
+
result.executed!
|
|
120
|
+
post_execution!
|
|
121
|
+
end
|
|
107
122
|
rescue StandardError => e
|
|
108
123
|
retry if retry_execution?(e)
|
|
109
|
-
result.fail!(
|
|
124
|
+
result.fail!(Utils::Normalize.exception(e), halt: false, cause: e, source: :exception)
|
|
110
125
|
raise_exception(e)
|
|
111
126
|
else
|
|
112
127
|
result.executed!
|
|
113
128
|
post_execution!
|
|
114
129
|
end
|
|
115
130
|
|
|
131
|
+
verify_middleware_yield!
|
|
116
132
|
finalize_execution!
|
|
117
133
|
end
|
|
118
134
|
|
|
119
135
|
protected
|
|
120
136
|
|
|
121
137
|
# Determines if execution should halt based on breakpoint configuration.
|
|
138
|
+
# Returns false when the result was created with +strict: false+.
|
|
122
139
|
#
|
|
123
140
|
# @param exception [Exception] The exception that occurred
|
|
124
141
|
#
|
|
@@ -126,10 +143,14 @@ module CMDx
|
|
|
126
143
|
#
|
|
127
144
|
# @rbs (Exception exception) -> bool
|
|
128
145
|
def halt_execution?(exception)
|
|
129
|
-
|
|
130
|
-
statuses = Array(statuses).map(&:to_s).uniq
|
|
146
|
+
return false unless exception.result.strict?
|
|
131
147
|
|
|
132
|
-
statuses
|
|
148
|
+
@halt_statuses ||= Utils::Normalize.statuses(
|
|
149
|
+
task.class.settings.breakpoints ||
|
|
150
|
+
task.class.settings.task_breakpoints
|
|
151
|
+
).freeze
|
|
152
|
+
|
|
153
|
+
@halt_statuses.include?(exception.result.status)
|
|
133
154
|
end
|
|
134
155
|
|
|
135
156
|
# Determines if execution should be retried based on retry configuration.
|
|
@@ -140,36 +161,22 @@ module CMDx
|
|
|
140
161
|
#
|
|
141
162
|
# @rbs (Exception exception) -> bool
|
|
142
163
|
def retry_execution?(exception)
|
|
143
|
-
|
|
144
|
-
return false unless available_retries.positive?
|
|
145
|
-
|
|
146
|
-
current_retry = result.retries
|
|
147
|
-
remaining_retries = available_retries - current_retry
|
|
148
|
-
return false unless remaining_retries.positive?
|
|
164
|
+
@retry ||= Retry.new(task)
|
|
149
165
|
|
|
150
|
-
|
|
151
|
-
return false unless
|
|
166
|
+
return false unless @retry.available? && @retry.remaining?
|
|
167
|
+
return false unless @retry.exception?(exception)
|
|
152
168
|
|
|
153
169
|
result.retries += 1
|
|
154
170
|
|
|
155
171
|
task.logger.warn do
|
|
156
|
-
reason =
|
|
157
|
-
task.to_h.merge!(reason:, remaining_retries:)
|
|
172
|
+
reason = Utils::Normalize.exception(exception)
|
|
173
|
+
task.to_h.merge!(reason:, remaining_retries: @retry.remaining)
|
|
158
174
|
end
|
|
159
175
|
|
|
160
|
-
|
|
161
|
-
jitter =
|
|
162
|
-
if jitter.is_a?(Symbol)
|
|
163
|
-
task.send(jitter, current_retry)
|
|
164
|
-
elsif jitter.is_a?(Proc)
|
|
165
|
-
task.instance_exec(current_retry, &jitter)
|
|
166
|
-
elsif jitter.respond_to?(:call)
|
|
167
|
-
jitter.call(task, current_retry)
|
|
168
|
-
else
|
|
169
|
-
jitter.to_f * current_retry
|
|
170
|
-
end
|
|
176
|
+
task.errors.clear
|
|
171
177
|
|
|
172
|
-
|
|
178
|
+
wait = @retry.wait
|
|
179
|
+
sleep(wait) if wait.positive?
|
|
173
180
|
|
|
174
181
|
true
|
|
175
182
|
end
|
|
@@ -198,7 +205,7 @@ module CMDx
|
|
|
198
205
|
#
|
|
199
206
|
# @rbs (Symbol type) -> void
|
|
200
207
|
def invoke_callbacks(type)
|
|
201
|
-
task.class.settings
|
|
208
|
+
task.class.settings.callbacks.invoke(type, task)
|
|
202
209
|
end
|
|
203
210
|
|
|
204
211
|
private
|
|
@@ -211,11 +218,12 @@ module CMDx
|
|
|
211
218
|
|
|
212
219
|
invoke_callbacks(:before_validation)
|
|
213
220
|
|
|
214
|
-
task.class.settings
|
|
221
|
+
task.class.settings.attributes.define_and_verify(task)
|
|
215
222
|
return if task.errors.empty?
|
|
216
223
|
|
|
217
224
|
result.fail!(
|
|
218
225
|
Locale.t("cmdx.faults.invalid"),
|
|
226
|
+
source: :validation,
|
|
219
227
|
errors: {
|
|
220
228
|
full_message: task.errors.to_s,
|
|
221
229
|
messages: task.errors.to_h
|
|
@@ -224,22 +232,23 @@ module CMDx
|
|
|
224
232
|
end
|
|
225
233
|
|
|
226
234
|
# Executes the main task logic.
|
|
235
|
+
# Wraps task.work in catch(:cmdx_halt) so that success! can halt early.
|
|
227
236
|
#
|
|
228
237
|
# @rbs () -> void
|
|
229
238
|
def execution!
|
|
230
239
|
invoke_callbacks(:before_execution)
|
|
231
240
|
|
|
232
241
|
result.executing!
|
|
233
|
-
task.work
|
|
242
|
+
catch(:cmdx_halt) { task.work }
|
|
234
243
|
end
|
|
235
244
|
|
|
236
245
|
# Verifies that all declared returns are present in the context after execution.
|
|
237
246
|
#
|
|
238
247
|
# @rbs () -> void
|
|
239
|
-
def
|
|
248
|
+
def verify_context_returns!
|
|
240
249
|
return unless result.success?
|
|
241
250
|
|
|
242
|
-
returns =
|
|
251
|
+
returns = Utils::Wrap.array(task.class.settings.returns)
|
|
243
252
|
missing = returns.reject { |name| task.context.key?(name) }
|
|
244
253
|
return if missing.empty?
|
|
245
254
|
|
|
@@ -247,6 +256,7 @@ module CMDx
|
|
|
247
256
|
|
|
248
257
|
result.fail!(
|
|
249
258
|
Locale.t("cmdx.faults.invalid"),
|
|
259
|
+
source: :context,
|
|
250
260
|
errors: {
|
|
251
261
|
full_message: task.errors.to_s,
|
|
252
262
|
messages: task.errors.to_h
|
|
@@ -258,20 +268,37 @@ module CMDx
|
|
|
258
268
|
#
|
|
259
269
|
# @rbs () -> void
|
|
260
270
|
def post_execution!
|
|
261
|
-
|
|
271
|
+
return if task.class.settings.callbacks.empty?
|
|
272
|
+
|
|
273
|
+
invoke_callbacks(STATE_CALLBACKS[result.state])
|
|
262
274
|
invoke_callbacks(:on_executed) if result.executed?
|
|
263
275
|
|
|
264
|
-
invoke_callbacks(
|
|
276
|
+
invoke_callbacks(STATUS_CALLBACKS[result.status])
|
|
265
277
|
invoke_callbacks(:on_good) if result.good?
|
|
266
278
|
invoke_callbacks(:on_bad) if result.bad?
|
|
267
279
|
end
|
|
268
280
|
|
|
281
|
+
# Detects if middleware swallowed the block without yielding.
|
|
282
|
+
# When this happens the result is still in the initialized state.
|
|
283
|
+
#
|
|
284
|
+
# @rbs () -> void
|
|
285
|
+
def verify_middleware_yield!
|
|
286
|
+
return unless result.initialized?
|
|
287
|
+
|
|
288
|
+
result.fail!(
|
|
289
|
+
Locale.t("cmdx.faults.invalid"),
|
|
290
|
+
halt: false,
|
|
291
|
+
source: :middleware
|
|
292
|
+
)
|
|
293
|
+
result.executed!
|
|
294
|
+
end
|
|
295
|
+
|
|
269
296
|
# Finalizes execution by freezing the task, logging results, and rolling back work.
|
|
270
297
|
#
|
|
271
|
-
# @rbs () ->
|
|
298
|
+
# @rbs () -> void
|
|
272
299
|
def finalize_execution!
|
|
273
300
|
log_execution!
|
|
274
|
-
log_backtrace! if task.class.settings
|
|
301
|
+
log_backtrace! if task.class.settings.backtrace
|
|
275
302
|
|
|
276
303
|
rollback_execution!
|
|
277
304
|
freeze_execution!
|
|
@@ -295,8 +322,8 @@ module CMDx
|
|
|
295
322
|
return if exception.nil? || exception.is_a?(Fault)
|
|
296
323
|
|
|
297
324
|
task.logger.error do
|
|
298
|
-
|
|
299
|
-
if (cleaner = task.class.settings
|
|
325
|
+
Utils::Normalize.exception(exception) << "\n" <<
|
|
326
|
+
if (cleaner = task.class.settings.backtrace_cleaner)
|
|
300
327
|
cleaner.call(exception.backtrace).join("\n\t")
|
|
301
328
|
else
|
|
302
329
|
exception.full_message(highlight: false)
|
|
@@ -309,25 +336,26 @@ module CMDx
|
|
|
309
336
|
# @rbs () -> void
|
|
310
337
|
def freeze_execution!
|
|
311
338
|
# Stubbing on frozen objects is not allowed in most test environments.
|
|
312
|
-
|
|
313
|
-
return if Coercions::Boolean.call(skip_freezing)
|
|
339
|
+
return unless CMDx.configuration.freeze_results
|
|
314
340
|
|
|
315
341
|
task.freeze
|
|
316
342
|
result.freeze
|
|
317
343
|
|
|
318
|
-
# Freezing the context and chain can only be done
|
|
319
|
-
#
|
|
344
|
+
# Freezing the context and chain can only be done once the outer-most
|
|
345
|
+
# task has completed.
|
|
320
346
|
return unless result.index.zero?
|
|
321
347
|
|
|
322
348
|
task.context.freeze
|
|
323
349
|
task.chain.freeze
|
|
324
350
|
end
|
|
325
351
|
|
|
326
|
-
# Clears the chain if the task is the outermost (top-level) task
|
|
352
|
+
# Clears the chain if the task is the outermost (top-level) task
|
|
353
|
+
# and the current thread's chain is the same instance this task belongs to.
|
|
327
354
|
#
|
|
328
355
|
# @rbs () -> void
|
|
329
356
|
def clear_chain!
|
|
330
357
|
return unless result.index.zero?
|
|
358
|
+
return unless Chain.current.equal?(task.chain)
|
|
331
359
|
|
|
332
360
|
Chain.clear
|
|
333
361
|
end
|
|
@@ -339,9 +367,8 @@ module CMDx
|
|
|
339
367
|
return if result.rolled_back?
|
|
340
368
|
return unless task.respond_to?(:rollback)
|
|
341
369
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
return unless statuses.include?(result.status)
|
|
370
|
+
@rollback_statuses ||= Utils::Normalize.statuses(task.class.settings.rollback_on).freeze
|
|
371
|
+
return unless @rollback_statuses.include?(result.status)
|
|
345
372
|
|
|
346
373
|
result.rolled_back = true
|
|
347
374
|
task.rollback
|
data/lib/cmdx/identifier.rb
CHANGED
|
@@ -20,12 +20,10 @@ module CMDx
|
|
|
20
20
|
# # => "01890b2c-1234-5678-9abc-def123456789"
|
|
21
21
|
#
|
|
22
22
|
# @rbs () -> String
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
SecureRandom.uuid
|
|
28
|
-
end
|
|
23
|
+
if SecureRandom.respond_to?(:uuid_v7)
|
|
24
|
+
def generate = SecureRandom.uuid_v7
|
|
25
|
+
else
|
|
26
|
+
def generate = SecureRandom.uuid
|
|
29
27
|
end
|
|
30
28
|
|
|
31
29
|
end
|
data/lib/cmdx/locale.rb
CHANGED
|
@@ -2,18 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
4
|
# Provides internationalization and localization support for CMDx.
|
|
5
|
-
# Handles translation lookups with fallback to
|
|
6
|
-
# when I18n gem is not available.
|
|
5
|
+
# Handles translation lookups with fallback to the configured locale's
|
|
6
|
+
# default messages when I18n gem is not available.
|
|
7
7
|
module Locale
|
|
8
8
|
|
|
9
9
|
extend self
|
|
10
10
|
|
|
11
|
-
# @rbs EN: Hash[String, untyped]
|
|
12
|
-
EN = YAML.load_file(CMDx.gem_path.join("lib/locales/en.yml")).freeze
|
|
13
|
-
private_constant :EN
|
|
14
|
-
|
|
15
11
|
# Translates a key to the current locale with optional interpolation.
|
|
16
|
-
# Falls back to
|
|
12
|
+
# Falls back to the configured locale's YAML file translations if
|
|
13
|
+
# I18n gem is unavailable.
|
|
17
14
|
#
|
|
18
15
|
# @param key [String, Symbol] The translation key (supports dot notation)
|
|
19
16
|
# @param options [Hash] Translation options
|
|
@@ -38,12 +35,12 @@ module CMDx
|
|
|
38
35
|
#
|
|
39
36
|
# @rbs ((String | Symbol) key, **untyped options) -> String
|
|
40
37
|
def translate(key, **options)
|
|
41
|
-
options[:default] ||=
|
|
38
|
+
options[:default] ||= translation_default(key)
|
|
42
39
|
return ::I18n.t(key, **options) if defined?(::I18n)
|
|
43
40
|
|
|
44
41
|
case message = options.delete(:default)
|
|
45
|
-
when NilClass then "Translation missing: #{key}"
|
|
46
42
|
when String then message % options
|
|
43
|
+
when NilClass then "Translation missing: #{key}"
|
|
47
44
|
else message
|
|
48
45
|
end
|
|
49
46
|
end
|
|
@@ -51,5 +48,31 @@ module CMDx
|
|
|
51
48
|
# @see #translate
|
|
52
49
|
alias t translate
|
|
53
50
|
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Resolves and caches the default translation for a key by digging
|
|
54
|
+
# into the configured locale's YAML translations.
|
|
55
|
+
#
|
|
56
|
+
# @param key [String, Symbol] The translation key
|
|
57
|
+
#
|
|
58
|
+
# @return [String, nil] The resolved translation or nil
|
|
59
|
+
#
|
|
60
|
+
# @rbs ((String | Symbol) key) -> String?
|
|
61
|
+
def translation_default(key)
|
|
62
|
+
tkey = "#{CMDx.configuration.default_locale}.#{key}"
|
|
63
|
+
|
|
64
|
+
@translation_defaults ||= {}
|
|
65
|
+
return @translation_defaults[tkey] if @translation_defaults.key?(tkey)
|
|
66
|
+
|
|
67
|
+
@default_translations ||= begin
|
|
68
|
+
path = CMDx.gem_path.join("lib/locales/#{CMDx.configuration.default_locale}.yml")
|
|
69
|
+
raise ArgumentError, "locale file not found: #{path}" unless path.exist?
|
|
70
|
+
|
|
71
|
+
YAML.load_file(path).freeze
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@translation_defaults[tkey] = @default_translations.dig(*tkey.split("."))
|
|
75
|
+
end
|
|
76
|
+
|
|
54
77
|
end
|
|
55
78
|
end
|
|
@@ -7,43 +7,48 @@ module CMDx
|
|
|
7
7
|
# that can be inserted, removed, and executed in sequence. Each middleware
|
|
8
8
|
# can be configured with specific options and is executed in the order
|
|
9
9
|
# they were registered.
|
|
10
|
+
#
|
|
11
|
+
# Supports copy-on-write semantics: a duped registry shares the parent's
|
|
12
|
+
# data until a write operation triggers materialization.
|
|
10
13
|
class MiddlewareRegistry
|
|
11
14
|
|
|
12
|
-
# Returns the ordered collection of middleware entries.
|
|
13
|
-
#
|
|
14
|
-
# @return [Array<Array>] Array of middleware-options pairs
|
|
15
|
-
#
|
|
16
|
-
# @example
|
|
17
|
-
# registry.registry # => [[LoggingMiddleware, {level: :debug}], [AuthMiddleware, {}]]
|
|
18
|
-
#
|
|
19
|
-
# @rbs @registry: Array[Array[untyped]]
|
|
20
|
-
attr_reader :registry
|
|
21
|
-
alias to_a registry
|
|
22
|
-
|
|
23
15
|
# Initialize a new middleware registry.
|
|
24
16
|
#
|
|
25
|
-
# @param registry [Array] Initial array of middleware entries
|
|
17
|
+
# @param registry [Array, nil] Initial array of middleware entries
|
|
26
18
|
#
|
|
27
19
|
# @example
|
|
28
20
|
# registry = MiddlewareRegistry.new
|
|
29
21
|
# registry = MiddlewareRegistry.new([[MyMiddleware, {option: 'value'}]])
|
|
30
22
|
#
|
|
31
|
-
# @rbs (?Array[Array[untyped]] registry) -> void
|
|
32
|
-
def initialize(registry =
|
|
33
|
-
@registry = registry
|
|
23
|
+
# @rbs (?Array[Array[untyped]]? registry) -> void
|
|
24
|
+
def initialize(registry = nil)
|
|
25
|
+
@registry = registry || []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Sets up copy-on-write state when duplicated via dup.
|
|
29
|
+
#
|
|
30
|
+
# @param source [MiddlewareRegistry] The registry being duplicated
|
|
31
|
+
#
|
|
32
|
+
# @rbs (MiddlewareRegistry source) -> void
|
|
33
|
+
def initialize_dup(source)
|
|
34
|
+
@parent = source
|
|
35
|
+
@registry = nil
|
|
36
|
+
super
|
|
34
37
|
end
|
|
35
38
|
|
|
36
|
-
#
|
|
39
|
+
# Returns the ordered collection of middleware entries.
|
|
40
|
+
# Delegates to the parent registry when not yet materialized.
|
|
37
41
|
#
|
|
38
|
-
# @return [
|
|
42
|
+
# @return [Array<Array>] Array of middleware-options pairs
|
|
39
43
|
#
|
|
40
44
|
# @example
|
|
41
|
-
#
|
|
45
|
+
# registry.registry # => [[LoggingMiddleware, {level: :debug}], [AuthMiddleware, {}]]
|
|
42
46
|
#
|
|
43
|
-
# @rbs () ->
|
|
44
|
-
def
|
|
45
|
-
|
|
47
|
+
# @rbs () -> Array[Array[untyped]]
|
|
48
|
+
def registry
|
|
49
|
+
@registry || @parent.registry
|
|
46
50
|
end
|
|
51
|
+
alias to_a registry
|
|
47
52
|
|
|
48
53
|
# Register a middleware component in the registry.
|
|
49
54
|
#
|
|
@@ -61,7 +66,9 @@ module CMDx
|
|
|
61
66
|
#
|
|
62
67
|
# @rbs (untyped middleware, ?at: Integer, **untyped options) -> self
|
|
63
68
|
def register(middleware, at: -1, **options)
|
|
64
|
-
|
|
69
|
+
materialize!
|
|
70
|
+
|
|
71
|
+
@registry.insert(at, [middleware, options])
|
|
65
72
|
self
|
|
66
73
|
end
|
|
67
74
|
|
|
@@ -76,7 +83,9 @@ module CMDx
|
|
|
76
83
|
#
|
|
77
84
|
# @rbs (untyped middleware) -> self
|
|
78
85
|
def deregister(middleware)
|
|
79
|
-
|
|
86
|
+
materialize!
|
|
87
|
+
|
|
88
|
+
@registry.reject! { |mw, _opts| mw == middleware }
|
|
80
89
|
self
|
|
81
90
|
end
|
|
82
91
|
|
|
@@ -105,6 +114,17 @@ module CMDx
|
|
|
105
114
|
|
|
106
115
|
private
|
|
107
116
|
|
|
117
|
+
# Copies the parent's registry data into this instance,
|
|
118
|
+
# severing the copy-on-write link.
|
|
119
|
+
#
|
|
120
|
+
# @rbs () -> void
|
|
121
|
+
def materialize!
|
|
122
|
+
return if @registry
|
|
123
|
+
|
|
124
|
+
@registry = @parent.registry.map(&:dup)
|
|
125
|
+
@parent = nil
|
|
126
|
+
end
|
|
127
|
+
|
|
108
128
|
# Recursively execute middleware in the chain.
|
|
109
129
|
#
|
|
110
130
|
# @param index [Integer] Current middleware index in the chain
|
|
@@ -129,8 +129,10 @@ module CMDx
|
|
|
129
129
|
# @return [Hash] The thread or fiber storage
|
|
130
130
|
#
|
|
131
131
|
# @rbs () -> Hash
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
if Fiber.respond_to?(:storage)
|
|
133
|
+
def thread_or_fiber = Fiber.storage
|
|
134
|
+
else
|
|
135
|
+
def thread_or_fiber = Thread.current
|
|
134
136
|
end
|
|
135
137
|
|
|
136
138
|
end
|
|
@@ -11,7 +11,8 @@ module CMDx
|
|
|
11
11
|
|
|
12
12
|
extend self
|
|
13
13
|
|
|
14
|
-
# Middleware entry point that measures task execution runtime
|
|
14
|
+
# Middleware entry point that measures task execution runtime and
|
|
15
|
+
# task execution start and end times.
|
|
15
16
|
#
|
|
16
17
|
# Evaluates the condition from options and measures execution time
|
|
17
18
|
# if enabled. Uses monotonic clock for precise timing measurements
|
|
@@ -35,11 +36,16 @@ module CMDx
|
|
|
35
36
|
#
|
|
36
37
|
# @rbs (Task task, **untyped options) { () -> untyped } -> untyped
|
|
37
38
|
def call(task, **options)
|
|
39
|
+
unow = utc_time
|
|
40
|
+
mnow = monotonic_time
|
|
38
41
|
return yield unless Utils::Condition.evaluate(task, options)
|
|
39
42
|
|
|
40
|
-
now = monotonic_time
|
|
41
43
|
result = yield
|
|
42
|
-
task.result.metadata
|
|
44
|
+
task.result.metadata.merge!(
|
|
45
|
+
runtime: monotonic_time - mnow,
|
|
46
|
+
ended_at: utc_time,
|
|
47
|
+
started_at: unow
|
|
48
|
+
)
|
|
43
49
|
result
|
|
44
50
|
end
|
|
45
51
|
|
|
@@ -57,6 +63,15 @@ module CMDx
|
|
|
57
63
|
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
58
64
|
end
|
|
59
65
|
|
|
66
|
+
# Gets the current UTC time in ISO 8601 format.
|
|
67
|
+
#
|
|
68
|
+
# @return [String] Current UTC time in ISO 8601 format
|
|
69
|
+
#
|
|
70
|
+
# @rbs () -> String
|
|
71
|
+
def utc_time
|
|
72
|
+
Time.now.utc.iso8601
|
|
73
|
+
end
|
|
74
|
+
|
|
60
75
|
end
|
|
61
76
|
end
|
|
62
77
|
end
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
# Error raised when task execution exceeds the configured timeout limit.
|
|
6
|
-
#
|
|
7
|
-
# This error occurs when a task takes longer to execute than the specified
|
|
8
|
-
# time limit. Timeout errors are raised by Ruby's Timeout module and are
|
|
9
|
-
# caught by the middleware to properly fail the task with timeout information.
|
|
10
|
-
TimeoutError = Class.new(Interrupt)
|
|
11
|
-
|
|
12
4
|
module Middlewares
|
|
13
5
|
# Middleware for enforcing execution time limits on tasks.
|
|
14
6
|
#
|
|
@@ -66,12 +58,21 @@ module CMDx
|
|
|
66
58
|
else callable.respond_to?(:call) ? callable.call(task) : DEFAULT_LIMIT
|
|
67
59
|
end
|
|
68
60
|
|
|
61
|
+
limit = Float(limit)
|
|
62
|
+
return yield unless limit.positive?
|
|
63
|
+
|
|
69
64
|
::Timeout.timeout(limit, TimeoutError, "execution exceeded #{limit} seconds", &)
|
|
70
65
|
rescue TimeoutError => e
|
|
71
|
-
task.result.tap
|
|
66
|
+
task.result.tap do |r|
|
|
67
|
+
r.fail!(
|
|
68
|
+
Utils::Normalize.exception(e),
|
|
69
|
+
cause: e,
|
|
70
|
+
source: :timeout,
|
|
71
|
+
limit:
|
|
72
|
+
)
|
|
73
|
+
end
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
end
|
|
75
77
|
end
|
|
76
|
-
|
|
77
78
|
end
|