cmdx 1.7.5 → 1.9.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/.cursor/prompts/docs.md +3 -3
- data/.cursor/prompts/llms.md +1 -3
- data/.cursor/prompts/rspec.md +1 -1
- data/.irbrc +14 -2
- data/CHANGELOG.md +62 -29
- data/LLM.md +203 -78
- data/README.md +23 -85
- data/docs/.DS_Store +0 -0
- data/docs/assets/favicon.ico +0 -0
- data/docs/assets/favicon.svg +1 -0
- data/docs/attributes/coercions.md +19 -29
- data/docs/attributes/defaults.md +3 -16
- data/docs/attributes/definitions.md +29 -39
- data/docs/attributes/naming.md +3 -13
- data/docs/attributes/transformations.md +63 -0
- data/docs/attributes/validations.md +23 -40
- data/docs/basics/chain.md +14 -23
- data/docs/basics/context.md +13 -22
- data/docs/basics/execution.md +8 -26
- data/docs/basics/setup.md +8 -19
- data/docs/callbacks.md +19 -32
- data/docs/deprecation.md +8 -25
- data/docs/getting_started.md +101 -77
- data/docs/index.md +120 -0
- data/docs/internationalization.md +6 -18
- data/docs/interruptions/exceptions.md +10 -16
- data/docs/interruptions/faults.md +8 -25
- data/docs/interruptions/halt.md +31 -25
- data/docs/logging.md +7 -17
- data/docs/middlewares.md +13 -29
- data/docs/outcomes/result.md +21 -38
- data/docs/outcomes/states.md +8 -22
- data/docs/outcomes/statuses.md +10 -21
- data/docs/stylesheets/extra.css +42 -0
- data/docs/tips_and_tricks.md +7 -46
- data/docs/workflows.md +23 -38
- data/examples/active_record_query_tagging.md +46 -0
- data/examples/paper_trail_whatdunnit.md +39 -0
- data/lib/cmdx/attribute.rb +9 -2
- data/lib/cmdx/attribute_value.rb +31 -10
- data/lib/cmdx/callback_registry.rb +12 -2
- data/lib/cmdx/coercions/hash.rb +6 -1
- data/lib/cmdx/configuration.rb +10 -2
- data/lib/cmdx/deprecator.rb +3 -3
- data/lib/cmdx/errors.rb +1 -1
- data/lib/cmdx/executor.rb +97 -9
- data/lib/cmdx/log_formatters/logstash.rb +4 -4
- data/lib/cmdx/pipeline.rb +4 -4
- data/lib/cmdx/railtie.rb +9 -0
- data/lib/cmdx/result.rb +10 -1
- data/lib/cmdx/task.rb +12 -7
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx.rb +1 -0
- data/lib/generators/cmdx/templates/install.rb +9 -0
- data/lib/locales/af.yml +2 -2
- data/lib/locales/ar.yml +2 -2
- data/lib/locales/az.yml +2 -2
- data/lib/locales/be.yml +2 -2
- data/lib/locales/bg.yml +2 -2
- data/lib/locales/bn.yml +2 -2
- data/lib/locales/bs.yml +2 -2
- data/lib/locales/ca.yml +2 -2
- data/lib/locales/cnr.yml +2 -2
- data/lib/locales/cs.yml +2 -2
- data/lib/locales/cy.yml +2 -2
- data/lib/locales/da.yml +2 -2
- data/lib/locales/de.yml +2 -2
- data/lib/locales/dz.yml +2 -2
- data/lib/locales/el.yml +2 -2
- data/lib/locales/en.yml +2 -2
- data/lib/locales/eo.yml +2 -2
- data/lib/locales/es.yml +2 -2
- data/lib/locales/et.yml +2 -2
- data/lib/locales/eu.yml +2 -2
- data/lib/locales/fa.yml +2 -2
- data/lib/locales/fi.yml +2 -2
- data/lib/locales/fr.yml +2 -2
- data/lib/locales/fy.yml +2 -2
- data/lib/locales/gd.yml +2 -2
- data/lib/locales/gl.yml +2 -2
- data/lib/locales/he.yml +2 -2
- data/lib/locales/hi.yml +2 -2
- data/lib/locales/hr.yml +2 -2
- data/lib/locales/hu.yml +2 -2
- data/lib/locales/hy.yml +2 -2
- data/lib/locales/id.yml +2 -2
- data/lib/locales/is.yml +2 -2
- data/lib/locales/it.yml +2 -2
- data/lib/locales/ja.yml +2 -2
- data/lib/locales/ka.yml +2 -2
- data/lib/locales/kk.yml +2 -2
- data/lib/locales/km.yml +2 -2
- data/lib/locales/kn.yml +2 -2
- data/lib/locales/ko.yml +2 -2
- data/lib/locales/lb.yml +2 -2
- data/lib/locales/lo.yml +2 -2
- data/lib/locales/lt.yml +2 -2
- data/lib/locales/lv.yml +2 -2
- data/lib/locales/mg.yml +2 -2
- data/lib/locales/mk.yml +2 -2
- data/lib/locales/ml.yml +2 -2
- data/lib/locales/mn.yml +2 -2
- data/lib/locales/mr-IN.yml +2 -2
- data/lib/locales/ms.yml +2 -2
- data/lib/locales/nb.yml +2 -2
- data/lib/locales/ne.yml +2 -2
- data/lib/locales/nl.yml +2 -2
- data/lib/locales/nn.yml +2 -2
- data/lib/locales/oc.yml +2 -2
- data/lib/locales/or.yml +2 -2
- data/lib/locales/pa.yml +2 -2
- data/lib/locales/pl.yml +2 -2
- data/lib/locales/pt.yml +2 -2
- data/lib/locales/rm.yml +2 -2
- data/lib/locales/ro.yml +2 -2
- data/lib/locales/ru.yml +2 -2
- data/lib/locales/sc.yml +2 -2
- data/lib/locales/sk.yml +2 -2
- data/lib/locales/sl.yml +2 -2
- data/lib/locales/sq.yml +2 -2
- data/lib/locales/sr.yml +2 -2
- data/lib/locales/st.yml +2 -2
- data/lib/locales/sv.yml +2 -2
- data/lib/locales/sw.yml +2 -2
- data/lib/locales/ta.yml +2 -2
- data/lib/locales/te.yml +2 -2
- data/lib/locales/th.yml +2 -2
- data/lib/locales/tl.yml +2 -2
- data/lib/locales/tr.yml +2 -2
- data/lib/locales/tt.yml +2 -2
- data/lib/locales/ug.yml +2 -2
- data/lib/locales/uk.yml +2 -2
- data/lib/locales/ur.yml +2 -2
- data/lib/locales/uz.yml +2 -2
- data/lib/locales/vi.yml +2 -2
- data/lib/locales/wo.yml +2 -2
- data/lib/locales/zh-CN.yml +2 -2
- data/lib/locales/zh-HK.yml +2 -2
- data/lib/locales/zh-TW.yml +2 -2
- data/lib/locales/zh-YUE.yml +2 -2
- data/mkdocs.yml +122 -0
- data/src/cmdx-dark-logo.png +0 -0
- data/src/cmdx-favicon.svg +1 -0
- data/src/cmdx-light-logo.png +0 -0
- data/src/cmdx-logo.svg +1 -0
- metadata +15 -4
- data/lib/cmdx/freezer.rb +0 -51
- data/src/cmdx-logo.png +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Paper Trail Whatdunnit
|
|
2
|
+
|
|
3
|
+
Tag paper trail version records with which service made a change with a custom `whatdunnit` attribute.
|
|
4
|
+
|
|
5
|
+
<https://github.com/paper-trail-gem/paper_trail?tab=readme-ov-file#4c-storing-metadata>
|
|
6
|
+
|
|
7
|
+
### Setup
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# lib/cmdx_paper_trail_middleware.rb
|
|
11
|
+
class CmdxPaperTrailMiddleware
|
|
12
|
+
def self.call(task, **options, &)
|
|
13
|
+
# This makes sure to reset the whatdunnit value to the previous
|
|
14
|
+
# value for nested task calls
|
|
15
|
+
|
|
16
|
+
begin
|
|
17
|
+
PaperTrail.request.controller_info ||= {}
|
|
18
|
+
old_whatdunnit = PaperTrail.request.controller_info[:whatdunnit]
|
|
19
|
+
PaperTrail.request.controller_info[:whatdunnit] = task.class.name
|
|
20
|
+
yield
|
|
21
|
+
ensure
|
|
22
|
+
PaperTrail.request.controller_info[:whatdunnit] = old_whatdunnit
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Usage
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
class MyTask < CMDx::Task
|
|
32
|
+
register :middleware, CmdxPaperTrailMiddleware
|
|
33
|
+
|
|
34
|
+
def work
|
|
35
|
+
# Do work...
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
|
39
|
+
```
|
data/lib/cmdx/attribute.rb
CHANGED
|
@@ -66,7 +66,7 @@ module CMDx
|
|
|
66
66
|
if names.none?
|
|
67
67
|
raise ArgumentError, "no attributes given"
|
|
68
68
|
elsif (names.size > 1) && options.key?(:as)
|
|
69
|
-
raise ArgumentError, ":as option only supports one attribute per definition"
|
|
69
|
+
raise ArgumentError, "the :as option only supports one attribute per definition"
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
names.filter_map { |name| new(name, **options, &) }
|
|
@@ -212,7 +212,14 @@ module CMDx
|
|
|
212
212
|
#
|
|
213
213
|
# @raise [RuntimeError] When the method name is already defined on the task
|
|
214
214
|
def define_and_verify
|
|
215
|
-
|
|
215
|
+
if task.respond_to?(method_name, true)
|
|
216
|
+
raise <<~MESSAGE
|
|
217
|
+
The method #{method_name.inspect} is already defined on the #{task.class.name} task.
|
|
218
|
+
This may be due conflicts with one of the task's user defined or internal methods/attributes.
|
|
219
|
+
|
|
220
|
+
Use :as, :prefix, and/or :suffix attribute options to avoid conflicts with existing methods.
|
|
221
|
+
MESSAGE
|
|
222
|
+
end
|
|
216
223
|
|
|
217
224
|
attribute_value = AttributeValue.new(self)
|
|
218
225
|
attribute_value.generate
|
data/lib/cmdx/attribute_value.rb
CHANGED
|
@@ -51,9 +51,10 @@ module CMDx
|
|
|
51
51
|
return if errors.for?(method_name)
|
|
52
52
|
|
|
53
53
|
coerced_value = coerce_value(derived_value)
|
|
54
|
+
transformed_value = transform_value(coerced_value)
|
|
54
55
|
return if errors.for?(method_name)
|
|
55
56
|
|
|
56
|
-
attributes[method_name] =
|
|
57
|
+
attributes[method_name] = transformed_value
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
# Validates the current attribute value against configured validators.
|
|
@@ -113,7 +114,7 @@ module CMDx
|
|
|
113
114
|
#
|
|
114
115
|
# @example
|
|
115
116
|
# # Default can be symbol, proc, or direct value
|
|
116
|
-
#
|
|
117
|
+
# -> { rand(100) } # => 23
|
|
117
118
|
def default_value
|
|
118
119
|
default = options[:default]
|
|
119
120
|
|
|
@@ -138,7 +139,7 @@ module CMDx
|
|
|
138
139
|
#
|
|
139
140
|
# @example
|
|
140
141
|
# # Derives from hash key, method call, or proc execution
|
|
141
|
-
#
|
|
142
|
+
# context.user_id # => 42
|
|
142
143
|
def derive_value(source_value)
|
|
143
144
|
derived_value =
|
|
144
145
|
case source_value
|
|
@@ -154,9 +155,29 @@ module CMDx
|
|
|
154
155
|
nil
|
|
155
156
|
end
|
|
156
157
|
|
|
158
|
+
# Transforms the derived value using the transform option.
|
|
159
|
+
#
|
|
160
|
+
# @param derived_value [Object] The value to transform
|
|
161
|
+
#
|
|
162
|
+
# @return [Object, nil] The transformed value or nil if transformation failed
|
|
163
|
+
#
|
|
164
|
+
# @example
|
|
165
|
+
# :downcase # => "hello"
|
|
166
|
+
def transform_value(derived_value)
|
|
167
|
+
transform = options[:transform]
|
|
168
|
+
|
|
169
|
+
if transform.is_a?(Symbol) && derived_value.respond_to?(transform, true)
|
|
170
|
+
derived_value.send(transform)
|
|
171
|
+
elsif transform.respond_to?(:call)
|
|
172
|
+
transform.call(derived_value)
|
|
173
|
+
else
|
|
174
|
+
derived_value
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
157
178
|
# Coerces the derived value to the expected type(s) using the coercion registry.
|
|
158
179
|
#
|
|
159
|
-
# @param
|
|
180
|
+
# @param transformed_value [Object] The value to coerce
|
|
160
181
|
#
|
|
161
182
|
# @return [Object, nil] The coerced value or nil if coercion failed
|
|
162
183
|
#
|
|
@@ -165,14 +186,14 @@ module CMDx
|
|
|
165
186
|
# @example
|
|
166
187
|
# # Coerces "42" to Integer, "true" to Boolean, etc.
|
|
167
188
|
# coerce_value("42") # => 42
|
|
168
|
-
def coerce_value(
|
|
169
|
-
return
|
|
189
|
+
def coerce_value(transformed_value)
|
|
190
|
+
return transformed_value if types.empty?
|
|
170
191
|
|
|
171
192
|
registry = task.class.settings[:coercions]
|
|
172
|
-
last_idx =
|
|
193
|
+
last_idx = types.size - 1
|
|
173
194
|
|
|
174
|
-
|
|
175
|
-
break registry.coerce(type, task,
|
|
195
|
+
types.find.with_index do |type, i|
|
|
196
|
+
break registry.coerce(type, task, transformed_value, options)
|
|
176
197
|
rescue CoercionError => e
|
|
177
198
|
next if i != last_idx
|
|
178
199
|
|
|
@@ -180,7 +201,7 @@ module CMDx
|
|
|
180
201
|
if last_idx.zero?
|
|
181
202
|
e.message
|
|
182
203
|
else
|
|
183
|
-
tl =
|
|
204
|
+
tl = types.map { |t| Locale.t("cmdx.types.#{t}") }.join(", ")
|
|
184
205
|
Locale.t("cmdx.coercions.into_any", types: tl)
|
|
185
206
|
end
|
|
186
207
|
|
|
@@ -95,9 +95,19 @@ module CMDx
|
|
|
95
95
|
raise TypeError, "unknown callback type #{type.inspect}" unless TYPES.include?(type)
|
|
96
96
|
|
|
97
97
|
Array(registry[type]).each do |callables, options|
|
|
98
|
-
next unless Utils::Condition.evaluate(task, options
|
|
98
|
+
next unless Utils::Condition.evaluate(task, options)
|
|
99
99
|
|
|
100
|
-
Array(callables).each
|
|
100
|
+
Array(callables).each do |callable|
|
|
101
|
+
if callable.is_a?(Symbol)
|
|
102
|
+
task.send(callable)
|
|
103
|
+
elsif callable.is_a?(Proc)
|
|
104
|
+
task.instance_exec(&callable)
|
|
105
|
+
elsif callable.respond_to?(:call)
|
|
106
|
+
callable.call(task)
|
|
107
|
+
else
|
|
108
|
+
raise "cannot invoke #{callable}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
101
111
|
end
|
|
102
112
|
end
|
|
103
113
|
|
data/lib/cmdx/coercions/hash.rb
CHANGED
|
@@ -5,6 +5,7 @@ module CMDx
|
|
|
5
5
|
# Coerces various input types into Hash objects
|
|
6
6
|
#
|
|
7
7
|
# Supports conversion from:
|
|
8
|
+
# - Nil values (converted to empty Hash)
|
|
8
9
|
# - Hash objects (returned as-is)
|
|
9
10
|
# - Array objects (converted using Hash[*array])
|
|
10
11
|
# - JSON strings starting with "{" (parsed into Hash)
|
|
@@ -30,12 +31,16 @@ module CMDx
|
|
|
30
31
|
# @example Coerce from JSON string
|
|
31
32
|
# Hash.call('{"key": "value"}') # => {"key" => "value"}
|
|
32
33
|
def call(value, options = {})
|
|
33
|
-
if value.
|
|
34
|
+
if value.nil?
|
|
35
|
+
{}
|
|
36
|
+
elsif value.is_a?(::Hash)
|
|
34
37
|
value
|
|
35
38
|
elsif value.is_a?(::Array)
|
|
36
39
|
::Hash[*value]
|
|
37
40
|
elsif value.is_a?(::String) && value.start_with?("{")
|
|
38
41
|
JSON.parse(value)
|
|
42
|
+
elsif value.respond_to?(:to_h)
|
|
43
|
+
value.to_h
|
|
39
44
|
else
|
|
40
45
|
raise_coercion_error!
|
|
41
46
|
end
|
data/lib/cmdx/configuration.rb
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
module CMDx
|
|
4
4
|
|
|
5
5
|
# Configuration class that manages global settings for CMDx including middlewares,
|
|
6
|
-
# callbacks, coercions, validators, breakpoints, and logging.
|
|
6
|
+
# callbacks, coercions, validators, breakpoints, backtraces, and logging.
|
|
7
7
|
class Configuration
|
|
8
8
|
|
|
9
9
|
DEFAULT_BREAKPOINTS = %w[failed].freeze
|
|
10
10
|
|
|
11
11
|
attr_accessor :middlewares, :callbacks, :coercions, :validators,
|
|
12
|
-
:task_breakpoints, :workflow_breakpoints, :logger
|
|
12
|
+
:task_breakpoints, :workflow_breakpoints, :logger,
|
|
13
|
+
:backtrace, :backtrace_cleaner, :exception_handler
|
|
13
14
|
|
|
14
15
|
# Initializes a new Configuration instance with default values.
|
|
15
16
|
#
|
|
@@ -31,6 +32,10 @@ module CMDx
|
|
|
31
32
|
@task_breakpoints = DEFAULT_BREAKPOINTS
|
|
32
33
|
@workflow_breakpoints = DEFAULT_BREAKPOINTS
|
|
33
34
|
|
|
35
|
+
@backtrace = false
|
|
36
|
+
@backtrace_cleaner = nil
|
|
37
|
+
@exception_handler = nil
|
|
38
|
+
|
|
34
39
|
@logger = Logger.new(
|
|
35
40
|
$stdout,
|
|
36
41
|
progname: "cmdx",
|
|
@@ -55,6 +60,9 @@ module CMDx
|
|
|
55
60
|
validators: @validators,
|
|
56
61
|
task_breakpoints: @task_breakpoints,
|
|
57
62
|
workflow_breakpoints: @workflow_breakpoints,
|
|
63
|
+
backtrace: @backtrace,
|
|
64
|
+
backtrace_cleaner: @backtrace_cleaner,
|
|
65
|
+
exception_handler: @exception_handler,
|
|
58
66
|
logger: @logger
|
|
59
67
|
}
|
|
60
68
|
end
|
data/lib/cmdx/deprecator.rb
CHANGED
|
@@ -44,15 +44,15 @@ module CMDx
|
|
|
44
44
|
# settings(deprecate: :warn)
|
|
45
45
|
# end
|
|
46
46
|
#
|
|
47
|
-
# MyTask.new # => [MyTask] DEPRECATED: migrate to replacement or discontinue use
|
|
47
|
+
# MyTask.new # => [MyTask] DEPRECATED: migrate to a replacement or discontinue use
|
|
48
48
|
def restrict(task)
|
|
49
49
|
type = EVAL.call(task, task.class.settings[:deprecate])
|
|
50
50
|
|
|
51
51
|
case type
|
|
52
52
|
when NilClass, FalseClass # Do nothing
|
|
53
53
|
when TrueClass, /raise/ then raise DeprecationError, "#{task.class.name} usage prohibited"
|
|
54
|
-
when /log/ then task.logger.warn { "DEPRECATED: migrate to replacement or discontinue use" }
|
|
55
|
-
when /warn/ then warn("[#{task.class.name}] DEPRECATED: migrate to replacement or discontinue use", category: :deprecated)
|
|
54
|
+
when /log/ then task.logger.warn { "DEPRECATED: migrate to a replacement or discontinue use" }
|
|
55
|
+
when /warn/ then warn("[#{task.class.name}] DEPRECATED: migrate to a replacement or discontinue use", category: :deprecated)
|
|
56
56
|
else raise "unknown deprecation type #{type.inspect}"
|
|
57
57
|
end
|
|
58
58
|
end
|
data/lib/cmdx/errors.rb
CHANGED
|
@@ -48,7 +48,7 @@ module CMDx
|
|
|
48
48
|
!messages[attribute].empty?
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
# Convert errors to a hash format with arrays of
|
|
51
|
+
# Convert errors to a hash format with arrays of full messages.
|
|
52
52
|
#
|
|
53
53
|
# @return [Hash{Symbol => Array<String>}] Hash with attribute keys and message arrays
|
|
54
54
|
#
|
data/lib/cmdx/executor.rb
CHANGED
|
@@ -46,14 +46,16 @@ module CMDx
|
|
|
46
46
|
# result = executor.execute
|
|
47
47
|
def execute
|
|
48
48
|
task.class.settings[:middlewares].call!(task) do
|
|
49
|
-
pre_execution!
|
|
49
|
+
pre_execution! unless @pre_execution
|
|
50
50
|
execution!
|
|
51
51
|
rescue UndefinedMethodError => e
|
|
52
52
|
raise(e) # No need to clear the Chain since exception is not being re-raised
|
|
53
53
|
rescue Fault => e
|
|
54
54
|
task.result.throw!(e.result, halt: false, cause: e)
|
|
55
55
|
rescue StandardError => e
|
|
56
|
+
retry if retry_execution?(e)
|
|
56
57
|
task.result.fail!("[#{e.class}] #{e.message}", halt: false, cause: e)
|
|
58
|
+
task.class.settings[:exception_handler]&.call(task, e)
|
|
57
59
|
ensure
|
|
58
60
|
task.result.executed!
|
|
59
61
|
post_execution!
|
|
@@ -73,7 +75,7 @@ module CMDx
|
|
|
73
75
|
# result = executor.execute!
|
|
74
76
|
def execute!
|
|
75
77
|
task.class.settings[:middlewares].call!(task) do
|
|
76
|
-
pre_execution!
|
|
78
|
+
pre_execution! unless @pre_execution
|
|
77
79
|
execution!
|
|
78
80
|
rescue UndefinedMethodError => e
|
|
79
81
|
raise_exception(e)
|
|
@@ -81,6 +83,7 @@ module CMDx
|
|
|
81
83
|
task.result.throw!(e.result, halt: false, cause: e)
|
|
82
84
|
halt_execution?(e) ? raise_exception(e) : post_execution!
|
|
83
85
|
rescue StandardError => e
|
|
86
|
+
retry if retry_execution?(e)
|
|
84
87
|
task.result.fail!("[#{e.class}] #{e.message}", halt: false, cause: e)
|
|
85
88
|
raise_exception(e)
|
|
86
89
|
else
|
|
@@ -108,6 +111,38 @@ module CMDx
|
|
|
108
111
|
breakpoints.include?(exception.result.status)
|
|
109
112
|
end
|
|
110
113
|
|
|
114
|
+
# Determines if execution should be retried based on retry configuration.
|
|
115
|
+
#
|
|
116
|
+
# @param exception [Exception] The exception that occurred
|
|
117
|
+
#
|
|
118
|
+
# @return [Boolean] Whether execution should be retried
|
|
119
|
+
#
|
|
120
|
+
# @example
|
|
121
|
+
# retry_execution?(standard_error)
|
|
122
|
+
def retry_execution?(exception)
|
|
123
|
+
available_retries = (task.class.settings[:retries] || 0).to_i
|
|
124
|
+
return false unless available_retries.positive?
|
|
125
|
+
|
|
126
|
+
current_retries = (task.result.metadata[:retries] ||= 0).to_i
|
|
127
|
+
remaining_retries = available_retries - current_retries
|
|
128
|
+
return false unless remaining_retries.positive?
|
|
129
|
+
|
|
130
|
+
exceptions = Array(task.class.settings[:retry_on] || StandardError)
|
|
131
|
+
return false unless exceptions.any? { |e| exception.class <= e }
|
|
132
|
+
|
|
133
|
+
task.result.metadata[:retries] += 1
|
|
134
|
+
|
|
135
|
+
task.logger.warn do
|
|
136
|
+
reason = "[#{exception.class}] #{exception.message}"
|
|
137
|
+
task.to_h.merge!(reason:, remaining_retries:)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
jitter = task.class.settings[:retry_jitter].to_f * current_retries
|
|
141
|
+
sleep(jitter) if jitter.positive?
|
|
142
|
+
|
|
143
|
+
true
|
|
144
|
+
end
|
|
145
|
+
|
|
111
146
|
# Raises an exception and clears the chain.
|
|
112
147
|
#
|
|
113
148
|
# @param exception [Exception] The exception to raise
|
|
@@ -118,6 +153,7 @@ module CMDx
|
|
|
118
153
|
# raise_exception(standard_error)
|
|
119
154
|
def raise_exception(exception)
|
|
120
155
|
Chain.clear
|
|
156
|
+
|
|
121
157
|
raise(exception)
|
|
122
158
|
end
|
|
123
159
|
|
|
@@ -135,8 +171,15 @@ module CMDx
|
|
|
135
171
|
|
|
136
172
|
private
|
|
137
173
|
|
|
174
|
+
# Lazy loaded repeator instance to handle retries.
|
|
175
|
+
def repeator
|
|
176
|
+
@repeator ||= Repeator.new(task)
|
|
177
|
+
end
|
|
178
|
+
|
|
138
179
|
# Performs pre-execution tasks including validation and attribute verification.
|
|
139
180
|
def pre_execution!
|
|
181
|
+
@pre_execution = true
|
|
182
|
+
|
|
140
183
|
invoke_callbacks(:before_validation)
|
|
141
184
|
|
|
142
185
|
task.class.settings[:attributes].define_and_verify(task)
|
|
@@ -144,8 +187,10 @@ module CMDx
|
|
|
144
187
|
|
|
145
188
|
task.result.fail!(
|
|
146
189
|
Locale.t("cmdx.faults.invalid"),
|
|
147
|
-
|
|
148
|
-
|
|
190
|
+
errors: {
|
|
191
|
+
full_message: task.errors.to_s,
|
|
192
|
+
messages: task.errors.to_h
|
|
193
|
+
}
|
|
149
194
|
)
|
|
150
195
|
end
|
|
151
196
|
|
|
@@ -169,13 +214,56 @@ module CMDx
|
|
|
169
214
|
|
|
170
215
|
# Finalizes execution by freezing the task and logging results.
|
|
171
216
|
def finalize_execution!
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
217
|
+
log_execution!
|
|
218
|
+
log_backtrace! if task.class.settings[:backtrace]
|
|
219
|
+
|
|
220
|
+
freeze_execution!
|
|
221
|
+
clear_chain!
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Logs the execution result at the configured log level.
|
|
225
|
+
def log_execution!
|
|
226
|
+
task.logger.info { task.result.to_h }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Logs the backtrace of the exception if the task failed.
|
|
230
|
+
def log_backtrace!
|
|
231
|
+
return unless task.result.failed?
|
|
232
|
+
|
|
233
|
+
exception = task.result.caused_failure.cause
|
|
234
|
+
return if exception.is_a?(Fault)
|
|
235
|
+
|
|
236
|
+
task.logger.error do
|
|
237
|
+
"[#{exception.class}] #{exception.message}\n" <<
|
|
238
|
+
if (cleaner = task.class.settings[:backtrace_cleaner])
|
|
239
|
+
cleaner.call(exception.backtrace).join("\n\t")
|
|
240
|
+
else
|
|
241
|
+
exception.full_message(highlight: false)
|
|
242
|
+
end
|
|
176
243
|
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Freezes the task and its associated objects to prevent modifications.
|
|
247
|
+
def freeze_execution!
|
|
248
|
+
# Stubbing on frozen objects is not allowed in most test environments.
|
|
249
|
+
skip_freezing = ENV.fetch("SKIP_CMDX_FREEZING", false)
|
|
250
|
+
return if Coercions::Boolean.call(skip_freezing)
|
|
251
|
+
|
|
252
|
+
task.freeze
|
|
253
|
+
task.result.freeze
|
|
254
|
+
|
|
255
|
+
# Freezing the context and chain can only be done
|
|
256
|
+
# once the outer-most task has completed.
|
|
257
|
+
return unless task.result.index.zero?
|
|
177
258
|
|
|
178
|
-
|
|
259
|
+
task.context.freeze
|
|
260
|
+
task.chain.freeze
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def clear_chain!
|
|
264
|
+
return unless task.result.index.zero?
|
|
265
|
+
|
|
266
|
+
Chain.clear
|
|
179
267
|
end
|
|
180
268
|
|
|
181
269
|
end
|
|
@@ -21,15 +21,15 @@ module CMDx
|
|
|
21
21
|
#
|
|
22
22
|
# @example Basic usage
|
|
23
23
|
# logger_formatter.call("INFO", Time.now, "MyApp", "User logged in")
|
|
24
|
-
# # => '{"
|
|
24
|
+
# # => '{"severity":"INFO","progname":"MyApp","pid":12345,"message":"User logged in","@version":"1","@timestamp":"2024-01-15T10:30:45.123456Z"}\n'
|
|
25
25
|
def call(severity, time, progname, message)
|
|
26
26
|
hash = {
|
|
27
|
-
"@version" => "1",
|
|
28
|
-
"@timestamp" => time.utc.iso8601(6),
|
|
29
27
|
severity:,
|
|
30
28
|
progname:,
|
|
31
29
|
pid: Process.pid,
|
|
32
|
-
message: Utils::Format.to_log(message)
|
|
30
|
+
message: Utils::Format.to_log(message),
|
|
31
|
+
"@version" => "1",
|
|
32
|
+
"@timestamp" => time.utc.iso8601(6)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
::JSON.dump(hash) << "\n"
|
data/lib/cmdx/pipeline.rb
CHANGED
|
@@ -42,7 +42,7 @@ module CMDx
|
|
|
42
42
|
# pipeline.execute
|
|
43
43
|
def execute
|
|
44
44
|
workflow.class.pipeline.each do |group|
|
|
45
|
-
next unless Utils::Condition.evaluate(workflow, group.options
|
|
45
|
+
next unless Utils::Condition.evaluate(workflow, group.options)
|
|
46
46
|
|
|
47
47
|
breakpoints = group.options[:breakpoints] ||
|
|
48
48
|
workflow.class.settings[:breakpoints] ||
|
|
@@ -108,18 +108,18 @@ module CMDx
|
|
|
108
108
|
# @example
|
|
109
109
|
# execute_tasks_in_parallel(group, ["failed"])
|
|
110
110
|
def execute_tasks_in_parallel(group, breakpoints)
|
|
111
|
-
raise "install the `parallel` gem to use this feature" unless defined?(
|
|
111
|
+
raise "install the `parallel` gem to use this feature" unless defined?(Parallel)
|
|
112
112
|
|
|
113
113
|
parallel_options = group.options.slice(:in_threads, :in_processes)
|
|
114
114
|
throwable_result = nil
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
Parallel.each(group.tasks, **parallel_options) do |task|
|
|
117
117
|
Chain.current = workflow.chain
|
|
118
118
|
|
|
119
119
|
task_result = task.execute(workflow.context)
|
|
120
120
|
next unless breakpoints.include?(task_result.status)
|
|
121
121
|
|
|
122
|
-
raise
|
|
122
|
+
raise Parallel::Break, throwable_result = task_result
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
return if throwable_result.nil?
|
data/lib/cmdx/railtie.rb
CHANGED
|
@@ -32,5 +32,14 @@ module CMDx
|
|
|
32
32
|
::I18n.reload!
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# Configures the backtrace cleaner for CMDx in a Rails environment.
|
|
36
|
+
#
|
|
37
|
+
# Sets the backtrace cleaner to the Rails backtrace cleaner.
|
|
38
|
+
initializer("cmdx.backtrace_cleaner") do
|
|
39
|
+
CMDx.configuration.backtrace_cleaner = lambda do |backtrace|
|
|
40
|
+
Rails.backtrace_cleaner.clean(backtrace)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
35
44
|
end
|
|
36
45
|
end
|
data/lib/cmdx/result.rb
CHANGED
|
@@ -291,7 +291,16 @@ module CMDx
|
|
|
291
291
|
|
|
292
292
|
# Strip the first two frames (this method and the delegator)
|
|
293
293
|
frames = caller_locations(3..-1)
|
|
294
|
-
|
|
294
|
+
|
|
295
|
+
unless frames.empty?
|
|
296
|
+
frames = frames.map(&:to_s)
|
|
297
|
+
|
|
298
|
+
if (cleaner = task.class.settings[:backtrace_cleaner])
|
|
299
|
+
cleaner.call(frames)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
fault.set_backtrace(frames)
|
|
303
|
+
end
|
|
295
304
|
|
|
296
305
|
raise(fault)
|
|
297
306
|
end
|
data/lib/cmdx/task.rb
CHANGED
|
@@ -40,9 +40,6 @@ module CMDx
|
|
|
40
40
|
class << self
|
|
41
41
|
|
|
42
42
|
# @param options [Hash] Configuration options to merge with existing settings
|
|
43
|
-
# @option options [AttributeRegistry] :attributes Registry for task attributes
|
|
44
|
-
# @option options [Boolean] :deprecate Whether the task is deprecated
|
|
45
|
-
# @option options [Array<Symbol>] :tags Tags associated with the task
|
|
46
43
|
#
|
|
47
44
|
# @return [Hash] The merged settings hash
|
|
48
45
|
#
|
|
@@ -54,13 +51,21 @@ module CMDx
|
|
|
54
51
|
@settings ||= begin
|
|
55
52
|
hash =
|
|
56
53
|
if superclass.respond_to?(:settings)
|
|
57
|
-
superclass.settings
|
|
54
|
+
parent = superclass.settings
|
|
55
|
+
parent
|
|
56
|
+
.except(:backtrace_cleaner, :exception_handler, :logger, :deprecate)
|
|
57
|
+
.transform_values!(&:dup)
|
|
58
|
+
.merge!(
|
|
59
|
+
backtrace_cleaner: parent[:backtrace_cleaner] || CMDx.configuration.backtrace_cleaner,
|
|
60
|
+
exception_handler: parent[:exception_handler] || CMDx.configuration.exception_handler,
|
|
61
|
+
logger: parent[:logger] || CMDx.configuration.logger,
|
|
62
|
+
deprecate: parent[:deprecate]
|
|
63
|
+
)
|
|
58
64
|
else
|
|
59
|
-
CMDx.configuration.to_h
|
|
60
|
-
end
|
|
65
|
+
CMDx.configuration.to_h
|
|
66
|
+
end
|
|
61
67
|
|
|
62
68
|
hash[:attributes] ||= AttributeRegistry.new
|
|
63
|
-
hash[:deprecate] ||= false
|
|
64
69
|
hash[:tags] ||= []
|
|
65
70
|
|
|
66
71
|
hash.merge!(options)
|
data/lib/cmdx/version.rb
CHANGED
data/lib/cmdx.rb
CHANGED
|
@@ -32,6 +32,15 @@ CMDx.configure do |config|
|
|
|
32
32
|
level: Logger::INFO
|
|
33
33
|
)
|
|
34
34
|
|
|
35
|
+
# Backtrace configuration - controls whether to log backtraces on faults and exceptions
|
|
36
|
+
# https://github.com/drexed/cmdx/blob/main/docs/getting_started.md#backtraces
|
|
37
|
+
# config.backtrace = false
|
|
38
|
+
# config.backtrace_cleaner = nil
|
|
39
|
+
|
|
40
|
+
# Exception handler configuration - called when non-fault exceptions are raised
|
|
41
|
+
# https://github.com/drexed/cmdx/blob/main/docs/getting_started.md#exception-handler
|
|
42
|
+
# config.exception_handler = nil
|
|
43
|
+
|
|
35
44
|
# Additional global configurations - automatically applied to all tasks
|
|
36
45
|
#
|
|
37
46
|
# Middlewares - https://github.com/drexed/cmdx/blob/main/docs/middlewares.md
|
data/lib/locales/af.yml
CHANGED
|
@@ -9,8 +9,8 @@ af:
|
|
|
9
9
|
into_any: "kon nie na een van: %{types} omskep word nie"
|
|
10
10
|
unknown: "onbekende %{type} omskep tipe"
|
|
11
11
|
faults:
|
|
12
|
-
invalid: "
|
|
13
|
-
unspecified: "
|
|
12
|
+
invalid: "Ongeldig"
|
|
13
|
+
unspecified: "Ongespesifiseer"
|
|
14
14
|
types:
|
|
15
15
|
array: "skikking"
|
|
16
16
|
big_decimal: "groot desimale"
|
data/lib/locales/ar.yml
CHANGED
|
@@ -9,8 +9,8 @@ ar:
|
|
|
9
9
|
into_any: "لا يمكن تحويله إلى واحد من: %{types}"
|
|
10
10
|
unknown: "نوع تحويل %{type} غير معروف"
|
|
11
11
|
faults:
|
|
12
|
-
invalid: "
|
|
13
|
-
unspecified: "
|
|
12
|
+
invalid: "غير صالح"
|
|
13
|
+
unspecified: "غير محدد"
|
|
14
14
|
types:
|
|
15
15
|
array: "مصفوفة"
|
|
16
16
|
big_decimal: "عدد عشري كبير"
|
data/lib/locales/az.yml
CHANGED
|
@@ -9,8 +9,8 @@ az:
|
|
|
9
9
|
into_any: "aşağıdakılardan birinə çevrilə bilmədi: %{types}"
|
|
10
10
|
unknown: "naməlum %{type} çevrilmə tipi"
|
|
11
11
|
faults:
|
|
12
|
-
invalid: "Etibarsız
|
|
13
|
-
unspecified: "
|
|
12
|
+
invalid: "Etibarsız"
|
|
13
|
+
unspecified: "Göstərilməyib"
|
|
14
14
|
types:
|
|
15
15
|
array: "massiv"
|
|
16
16
|
big_decimal: "böyük onluq"
|
data/lib/locales/be.yml
CHANGED
|
@@ -9,8 +9,8 @@ be:
|
|
|
9
9
|
into_any: "не ўдалося пераўтварыць у адзін з: %{types}"
|
|
10
10
|
unknown: "невядомы тып пераўтварэння %{type}"
|
|
11
11
|
faults:
|
|
12
|
-
invalid: "Няправільныя
|
|
13
|
-
unspecified: "
|
|
12
|
+
invalid: "Няправільныя"
|
|
13
|
+
unspecified: "Не паказана"
|
|
14
14
|
types:
|
|
15
15
|
array: "масіў"
|
|
16
16
|
big_decimal: "вялікае дзесятковае лік"
|
data/lib/locales/bg.yml
CHANGED
|
@@ -9,8 +9,8 @@ bg:
|
|
|
9
9
|
into_any: "не може да бъде преобразуван в един от: %{types}"
|
|
10
10
|
unknown: "неизвестен тип преобразуване %{type}"
|
|
11
11
|
faults:
|
|
12
|
-
invalid: "Невалидни
|
|
13
|
-
unspecified: "Не е посочена
|
|
12
|
+
invalid: "Невалидни"
|
|
13
|
+
unspecified: "Не е посочена"
|
|
14
14
|
types:
|
|
15
15
|
array: "масив"
|
|
16
16
|
big_decimal: "голямо десетично число"
|
data/lib/locales/bn.yml
CHANGED
|
@@ -9,8 +9,8 @@ bn:
|
|
|
9
9
|
into_any: "নিম্নলিখিতগুলির মধ্যে একটিতে রূপান্তর করা যায়নি: %{types}"
|
|
10
10
|
unknown: "অজানা %{type} রূপান্তর প্রকার"
|
|
11
11
|
faults:
|
|
12
|
-
invalid: "অবৈধ
|
|
13
|
-
unspecified: "
|
|
12
|
+
invalid: "অবৈধ"
|
|
13
|
+
unspecified: "অনির্দিষ্ট"
|
|
14
14
|
types:
|
|
15
15
|
array: "অ্যারে"
|
|
16
16
|
big_decimal: "বড় দশমিক"
|