cmdx 1.8.0 → 1.9.1
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/yardoc.md +1 -0
- data/.irbrc +14 -2
- data/CHANGELOG.md +64 -45
- data/LLM.md +159 -53
- data/README.md +26 -83
- 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 +12 -24
- data/docs/attributes/defaults.md +3 -16
- data/docs/attributes/definitions.md +16 -30
- data/docs/attributes/naming.md +3 -13
- data/docs/attributes/transformations.md +63 -0
- data/docs/attributes/validations.md +14 -33
- 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 +109 -76
- data/docs/index.md +132 -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 +12 -27
- 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 +88 -6
- data/lib/cmdx/attribute_registry.rb +20 -0
- data/lib/cmdx/attribute_value.rb +56 -10
- data/lib/cmdx/callback_registry.rb +31 -2
- data/lib/cmdx/chain.rb +34 -1
- data/lib/cmdx/coercion_registry.rb +18 -0
- data/lib/cmdx/coercions/array.rb +2 -0
- data/lib/cmdx/coercions/big_decimal.rb +3 -0
- data/lib/cmdx/coercions/boolean.rb +5 -0
- data/lib/cmdx/coercions/complex.rb +2 -0
- data/lib/cmdx/coercions/date.rb +4 -0
- data/lib/cmdx/coercions/date_time.rb +5 -0
- data/lib/cmdx/coercions/float.rb +2 -0
- data/lib/cmdx/coercions/hash.rb +4 -0
- data/lib/cmdx/coercions/integer.rb +2 -0
- data/lib/cmdx/coercions/rational.rb +2 -0
- data/lib/cmdx/coercions/string.rb +2 -0
- data/lib/cmdx/coercions/symbol.rb +2 -0
- data/lib/cmdx/coercions/time.rb +5 -0
- data/lib/cmdx/configuration.rb +119 -3
- data/lib/cmdx/context.rb +36 -0
- data/lib/cmdx/deprecator.rb +6 -3
- data/lib/cmdx/errors.rb +22 -0
- data/lib/cmdx/executor.rb +136 -7
- data/lib/cmdx/faults.rb +14 -0
- data/lib/cmdx/identifier.rb +2 -0
- data/lib/cmdx/locale.rb +3 -0
- data/lib/cmdx/log_formatters/json.rb +2 -0
- data/lib/cmdx/log_formatters/key_value.rb +2 -0
- data/lib/cmdx/log_formatters/line.rb +2 -0
- data/lib/cmdx/log_formatters/logstash.rb +2 -0
- data/lib/cmdx/log_formatters/raw.rb +2 -0
- data/lib/cmdx/middleware_registry.rb +20 -0
- data/lib/cmdx/middlewares/correlate.rb +11 -0
- data/lib/cmdx/middlewares/runtime.rb +4 -0
- data/lib/cmdx/middlewares/timeout.rb +4 -0
- data/lib/cmdx/pipeline.rb +24 -5
- data/lib/cmdx/railtie.rb +13 -0
- data/lib/cmdx/result.rb +133 -2
- data/lib/cmdx/task.rb +103 -8
- data/lib/cmdx/utils/call.rb +2 -0
- data/lib/cmdx/utils/condition.rb +3 -0
- data/lib/cmdx/utils/format.rb +5 -0
- data/lib/cmdx/validator_registry.rb +18 -0
- data/lib/cmdx/validators/exclusion.rb +2 -0
- data/lib/cmdx/validators/format.rb +2 -0
- data/lib/cmdx/validators/inclusion.rb +2 -0
- data/lib/cmdx/validators/length.rb +14 -0
- data/lib/cmdx/validators/numeric.rb +14 -0
- data/lib/cmdx/validators/presence.rb +2 -0
- data/lib/cmdx/version.rb +4 -1
- data/lib/cmdx/workflow.rb +10 -0
- data/lib/cmdx.rb +9 -0
- data/lib/generators/cmdx/locale_generator.rb +0 -1
- data/lib/generators/cmdx/templates/install.rb +9 -0
- 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 +14 -3
- data/lib/cmdx/freezer.rb +0 -51
- data/src/cmdx-logo.png +0 -0
data/lib/cmdx/configuration.rb
CHANGED
|
@@ -3,13 +3,112 @@
|
|
|
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
|
+
# @rbs DEFAULT_BREAKPOINTS: Array[String]
|
|
9
10
|
DEFAULT_BREAKPOINTS = %w[failed].freeze
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
# Returns the middleware registry for task execution.
|
|
13
|
+
#
|
|
14
|
+
# @return [MiddlewareRegistry] The middleware registry
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# config.middlewares.register(CustomMiddleware)
|
|
18
|
+
#
|
|
19
|
+
# @rbs @middlewares: MiddlewareRegistry
|
|
20
|
+
attr_accessor :middlewares
|
|
21
|
+
|
|
22
|
+
# Returns the callback registry for task lifecycle hooks.
|
|
23
|
+
#
|
|
24
|
+
# @return [CallbackRegistry] The callback registry
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# config.callbacks.register(:before_execution, :log_start)
|
|
28
|
+
#
|
|
29
|
+
# @rbs @callbacks: CallbackRegistry
|
|
30
|
+
attr_accessor :callbacks
|
|
31
|
+
|
|
32
|
+
# Returns the coercion registry for type conversions.
|
|
33
|
+
#
|
|
34
|
+
# @return [CoercionRegistry] The coercion registry
|
|
35
|
+
#
|
|
36
|
+
# @example
|
|
37
|
+
# config.coercions.register(:custom, CustomCoercion)
|
|
38
|
+
#
|
|
39
|
+
# @rbs @coercions: CoercionRegistry
|
|
40
|
+
attr_accessor :coercions
|
|
41
|
+
|
|
42
|
+
# Returns the validator registry for attribute validation.
|
|
43
|
+
#
|
|
44
|
+
# @return [ValidatorRegistry] The validator registry
|
|
45
|
+
#
|
|
46
|
+
# @example
|
|
47
|
+
# config.validators.register(:email, EmailValidator)
|
|
48
|
+
#
|
|
49
|
+
# @rbs @validators: ValidatorRegistry
|
|
50
|
+
attr_accessor :validators
|
|
51
|
+
|
|
52
|
+
# Returns the breakpoint statuses for task execution interruption.
|
|
53
|
+
#
|
|
54
|
+
# @return [Array<String>] Array of status names that trigger breakpoints
|
|
55
|
+
#
|
|
56
|
+
# @example
|
|
57
|
+
# config.task_breakpoints = ["failed", "skipped"]
|
|
58
|
+
#
|
|
59
|
+
# @rbs @task_breakpoints: Array[String]
|
|
60
|
+
attr_accessor :task_breakpoints
|
|
61
|
+
|
|
62
|
+
# Returns the breakpoint statuses for workflow execution interruption.
|
|
63
|
+
#
|
|
64
|
+
# @return [Array<String>] Array of status names that trigger breakpoints
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# config.workflow_breakpoints = ["failed", "skipped"]
|
|
68
|
+
#
|
|
69
|
+
# @rbs @task_breakpoints: Array[String]
|
|
70
|
+
# @rbs @workflow_breakpoints: Array[String]
|
|
71
|
+
attr_accessor :workflow_breakpoints
|
|
72
|
+
|
|
73
|
+
# Returns the logger instance for CMDx operations.
|
|
74
|
+
#
|
|
75
|
+
# @return [Logger] The logger instance
|
|
76
|
+
#
|
|
77
|
+
# @example
|
|
78
|
+
# config.logger.level = Logger::DEBUG
|
|
79
|
+
#
|
|
80
|
+
# @rbs @logger: Logger
|
|
81
|
+
attr_accessor :logger
|
|
82
|
+
|
|
83
|
+
# Returns whether to log backtraces for failed tasks.
|
|
84
|
+
#
|
|
85
|
+
# @return [Boolean] true if backtraces should be logged
|
|
86
|
+
#
|
|
87
|
+
# @example
|
|
88
|
+
# config.backtrace = true
|
|
89
|
+
#
|
|
90
|
+
# @rbs @backtrace: bool
|
|
91
|
+
attr_accessor :backtrace
|
|
92
|
+
|
|
93
|
+
# Returns the proc used to clean backtraces before logging.
|
|
94
|
+
#
|
|
95
|
+
# @return [Proc, nil] The backtrace cleaner proc, or nil if not set
|
|
96
|
+
#
|
|
97
|
+
# @example
|
|
98
|
+
# config.backtrace_cleaner = ->(bt) { bt.first(5) }
|
|
99
|
+
#
|
|
100
|
+
# @rbs @backtrace_cleaner: (Proc | nil)
|
|
101
|
+
attr_accessor :backtrace_cleaner
|
|
102
|
+
|
|
103
|
+
# Returns the proc called when exceptions occur during execution.
|
|
104
|
+
#
|
|
105
|
+
# @return [Proc, nil] The exception handler proc, or nil if not set
|
|
106
|
+
#
|
|
107
|
+
# @example
|
|
108
|
+
# config.exception_handler = ->(task, error) { Sentry.capture_exception(error) }
|
|
109
|
+
#
|
|
110
|
+
# @rbs @exception_handler: (Proc | nil)
|
|
111
|
+
attr_accessor :exception_handler
|
|
13
112
|
|
|
14
113
|
# Initializes a new Configuration instance with default values.
|
|
15
114
|
#
|
|
@@ -22,6 +121,8 @@ module CMDx
|
|
|
22
121
|
# config = Configuration.new
|
|
23
122
|
# config.middlewares.class # => MiddlewareRegistry
|
|
24
123
|
# config.task_breakpoints # => ["failed"]
|
|
124
|
+
#
|
|
125
|
+
# @rbs () -> void
|
|
25
126
|
def initialize
|
|
26
127
|
@middlewares = MiddlewareRegistry.new
|
|
27
128
|
@callbacks = CallbackRegistry.new
|
|
@@ -31,6 +132,10 @@ module CMDx
|
|
|
31
132
|
@task_breakpoints = DEFAULT_BREAKPOINTS
|
|
32
133
|
@workflow_breakpoints = DEFAULT_BREAKPOINTS
|
|
33
134
|
|
|
135
|
+
@backtrace = false
|
|
136
|
+
@backtrace_cleaner = nil
|
|
137
|
+
@exception_handler = nil
|
|
138
|
+
|
|
34
139
|
@logger = Logger.new(
|
|
35
140
|
$stdout,
|
|
36
141
|
progname: "cmdx",
|
|
@@ -47,6 +152,8 @@ module CMDx
|
|
|
47
152
|
# config = Configuration.new
|
|
48
153
|
# config.to_h
|
|
49
154
|
# # => { middlewares: #<MiddlewareRegistry>, callbacks: #<CallbackRegistry>, ... }
|
|
155
|
+
#
|
|
156
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
50
157
|
def to_h
|
|
51
158
|
{
|
|
52
159
|
middlewares: @middlewares,
|
|
@@ -55,6 +162,9 @@ module CMDx
|
|
|
55
162
|
validators: @validators,
|
|
56
163
|
task_breakpoints: @task_breakpoints,
|
|
57
164
|
workflow_breakpoints: @workflow_breakpoints,
|
|
165
|
+
backtrace: @backtrace,
|
|
166
|
+
backtrace_cleaner: @backtrace_cleaner,
|
|
167
|
+
exception_handler: @exception_handler,
|
|
58
168
|
logger: @logger
|
|
59
169
|
}
|
|
60
170
|
end
|
|
@@ -70,6 +180,8 @@ module CMDx
|
|
|
70
180
|
# @example
|
|
71
181
|
# config = CMDx.configuration
|
|
72
182
|
# config.middlewares # => #<MiddlewareRegistry>
|
|
183
|
+
#
|
|
184
|
+
# @rbs () -> Configuration
|
|
73
185
|
def configuration
|
|
74
186
|
return @configuration if @configuration
|
|
75
187
|
|
|
@@ -91,6 +203,8 @@ module CMDx
|
|
|
91
203
|
# config.task_breakpoints = ["failed", "skipped"]
|
|
92
204
|
# config.logger.level = Logger::DEBUG
|
|
93
205
|
# end
|
|
206
|
+
#
|
|
207
|
+
# @rbs () { (Configuration) -> void } -> Configuration
|
|
94
208
|
def configure
|
|
95
209
|
raise ArgumentError, "block required" unless block_given?
|
|
96
210
|
|
|
@@ -106,6 +220,8 @@ module CMDx
|
|
|
106
220
|
# @example
|
|
107
221
|
# CMDx.reset_configuration!
|
|
108
222
|
# # Configuration is now reset to defaults
|
|
223
|
+
#
|
|
224
|
+
# @rbs () -> Configuration
|
|
109
225
|
def reset_configuration!
|
|
110
226
|
@configuration = Configuration.new
|
|
111
227
|
end
|
data/lib/cmdx/context.rb
CHANGED
|
@@ -11,6 +11,14 @@ module CMDx
|
|
|
11
11
|
|
|
12
12
|
extend Forwardable
|
|
13
13
|
|
|
14
|
+
# Returns the internal hash storing context data.
|
|
15
|
+
#
|
|
16
|
+
# @return [Hash{Symbol => Object}] The internal hash table
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# context.table # => { name: "John", age: 30 }
|
|
20
|
+
#
|
|
21
|
+
# @rbs @table: Hash[Symbol, untyped]
|
|
14
22
|
attr_reader :table
|
|
15
23
|
alias to_h table
|
|
16
24
|
|
|
@@ -28,6 +36,8 @@ module CMDx
|
|
|
28
36
|
# @example
|
|
29
37
|
# context = Context.new(name: "John", age: 30)
|
|
30
38
|
# context[:name] # => "John"
|
|
39
|
+
#
|
|
40
|
+
# @rbs (untyped args) -> void
|
|
31
41
|
def initialize(args = {})
|
|
32
42
|
@table =
|
|
33
43
|
if args.respond_to?(:to_hash)
|
|
@@ -50,6 +60,8 @@ module CMDx
|
|
|
50
60
|
# existing = Context.new(name: "John")
|
|
51
61
|
# built = Context.build(existing) # reuses existing context
|
|
52
62
|
# built.object_id == existing.object_id # => true
|
|
63
|
+
#
|
|
64
|
+
# @rbs (untyped context) -> Context
|
|
53
65
|
def self.build(context = {})
|
|
54
66
|
if context.is_a?(self) && !context.frozen?
|
|
55
67
|
context
|
|
@@ -70,6 +82,8 @@ module CMDx
|
|
|
70
82
|
# context = Context.new(name: "John")
|
|
71
83
|
# context[:name] # => "John"
|
|
72
84
|
# context["name"] # => "John" (automatically converted to symbol)
|
|
85
|
+
#
|
|
86
|
+
# @rbs ((String | Symbol) key) -> untyped
|
|
73
87
|
def [](key)
|
|
74
88
|
table[key.to_sym]
|
|
75
89
|
end
|
|
@@ -85,6 +99,8 @@ module CMDx
|
|
|
85
99
|
# context = Context.new
|
|
86
100
|
# context.store(:name, "John")
|
|
87
101
|
# context[:name] # => "John"
|
|
102
|
+
#
|
|
103
|
+
# @rbs ((String | Symbol) key, untyped value) -> untyped
|
|
88
104
|
def store(key, value)
|
|
89
105
|
table[key.to_sym] = value
|
|
90
106
|
end
|
|
@@ -104,6 +120,8 @@ module CMDx
|
|
|
104
120
|
# context.fetch(:name) # => "John"
|
|
105
121
|
# context.fetch(:age, 25) # => 25
|
|
106
122
|
# context.fetch(:city) { |key| "Unknown #{key}" } # => "Unknown city"
|
|
123
|
+
#
|
|
124
|
+
# @rbs ((String | Symbol) key, *untyped) ?{ ((String | Symbol)) -> untyped } -> untyped
|
|
107
125
|
def fetch(key, ...)
|
|
108
126
|
table.fetch(key.to_sym, ...)
|
|
109
127
|
end
|
|
@@ -122,6 +140,8 @@ module CMDx
|
|
|
122
140
|
# context.fetch_or_store(:name, "Default") # => "John" (existing value)
|
|
123
141
|
# context.fetch_or_store(:age, 25) # => 25 (stored and returned)
|
|
124
142
|
# context.fetch_or_store(:city) { |key| "Unknown #{key}" } # => "Unknown city" (stored and returned)
|
|
143
|
+
#
|
|
144
|
+
# @rbs ((String | Symbol) key, ?untyped value) ?{ () -> untyped } -> untyped
|
|
125
145
|
def fetch_or_store(key, value = nil)
|
|
126
146
|
table.fetch(key.to_sym) do
|
|
127
147
|
table[key.to_sym] = block_given? ? yield : value
|
|
@@ -139,6 +159,8 @@ module CMDx
|
|
|
139
159
|
# context = Context.new(name: "John")
|
|
140
160
|
# context.merge!(age: 30, city: "NYC")
|
|
141
161
|
# context.to_h # => {name: "John", age: 30, city: "NYC"}
|
|
162
|
+
#
|
|
163
|
+
# @rbs (?untyped args) -> self
|
|
142
164
|
def merge!(args = {})
|
|
143
165
|
args.to_h.each { |key, value| self[key.to_sym] = value }
|
|
144
166
|
self
|
|
@@ -156,6 +178,8 @@ module CMDx
|
|
|
156
178
|
# context = Context.new(name: "John", age: 30)
|
|
157
179
|
# context.delete!(:age) # => 30
|
|
158
180
|
# context.delete!(:city) { |key| "Key #{key} not found" } # => "Key city not found"
|
|
181
|
+
#
|
|
182
|
+
# @rbs ((String | Symbol) key) ?{ ((String | Symbol)) -> untyped } -> untyped
|
|
159
183
|
def delete!(key, &)
|
|
160
184
|
table.delete(key.to_sym, &)
|
|
161
185
|
end
|
|
@@ -170,6 +194,8 @@ module CMDx
|
|
|
170
194
|
# context1 = Context.new(name: "John")
|
|
171
195
|
# context2 = Context.new(name: "John")
|
|
172
196
|
# context1 == context2 # => true
|
|
197
|
+
#
|
|
198
|
+
# @rbs (untyped other) -> bool
|
|
173
199
|
def eql?(other)
|
|
174
200
|
other.is_a?(self.class) && (to_h == other.to_h)
|
|
175
201
|
end
|
|
@@ -185,6 +211,8 @@ module CMDx
|
|
|
185
211
|
# context = Context.new(name: "John")
|
|
186
212
|
# context.key?(:name) # => true
|
|
187
213
|
# context.key?(:age) # => false
|
|
214
|
+
#
|
|
215
|
+
# @rbs ((String | Symbol) key) -> bool
|
|
188
216
|
def key?(key)
|
|
189
217
|
table.key?(key.to_sym)
|
|
190
218
|
end
|
|
@@ -200,6 +228,8 @@ module CMDx
|
|
|
200
228
|
# context = Context.new(user: {profile: {name: "John"}})
|
|
201
229
|
# context.dig(:user, :profile, :name) # => "John"
|
|
202
230
|
# context.dig(:user, :profile, :age) # => nil
|
|
231
|
+
#
|
|
232
|
+
# @rbs ((String | Symbol) key, *(String | Symbol) keys) -> untyped
|
|
203
233
|
def dig(key, *keys)
|
|
204
234
|
table.dig(key.to_sym, *keys)
|
|
205
235
|
end
|
|
@@ -211,6 +241,8 @@ module CMDx
|
|
|
211
241
|
# @example
|
|
212
242
|
# context = Context.new(name: "John", age: 30)
|
|
213
243
|
# context.to_s # => "name: John, age: 30"
|
|
244
|
+
#
|
|
245
|
+
# @rbs () -> String
|
|
214
246
|
def to_s
|
|
215
247
|
Utils::Format.to_str(to_h)
|
|
216
248
|
end
|
|
@@ -227,6 +259,8 @@ module CMDx
|
|
|
227
259
|
# @yield [Object] optional block
|
|
228
260
|
#
|
|
229
261
|
# @return [Object] the result of the method call
|
|
262
|
+
#
|
|
263
|
+
# @rbs (Symbol method_name, *untyped args, **untyped _kwargs) ?{ () -> untyped } -> untyped
|
|
230
264
|
def method_missing(method_name, *args, **_kwargs, &)
|
|
231
265
|
fetch(method_name) do
|
|
232
266
|
store(method_name[0..-2], args.first) if method_name.end_with?("=")
|
|
@@ -244,6 +278,8 @@ module CMDx
|
|
|
244
278
|
# context = Context.new(name: "John")
|
|
245
279
|
# context.respond_to?(:name) # => true
|
|
246
280
|
# context.respond_to?(:age) # => false
|
|
281
|
+
#
|
|
282
|
+
# @rbs (Symbol method_name, ?bool include_private) -> bool
|
|
247
283
|
def respond_to_missing?(method_name, include_private = false)
|
|
248
284
|
key?(method_name) || super
|
|
249
285
|
end
|
data/lib/cmdx/deprecator.rb
CHANGED
|
@@ -10,6 +10,7 @@ module CMDx
|
|
|
10
10
|
|
|
11
11
|
extend self
|
|
12
12
|
|
|
13
|
+
# @rbs EVAL: Proc
|
|
13
14
|
EVAL = proc do |target, callable|
|
|
14
15
|
case callable
|
|
15
16
|
when /raise|log|warn/ then callable
|
|
@@ -44,15 +45,17 @@ module CMDx
|
|
|
44
45
|
# settings(deprecate: :warn)
|
|
45
46
|
# end
|
|
46
47
|
#
|
|
47
|
-
# MyTask.new # => [MyTask] DEPRECATED: migrate to replacement or discontinue use
|
|
48
|
+
# MyTask.new # => [MyTask] DEPRECATED: migrate to a replacement or discontinue use
|
|
49
|
+
#
|
|
50
|
+
# @rbs (Task task) -> void
|
|
48
51
|
def restrict(task)
|
|
49
52
|
type = EVAL.call(task, task.class.settings[:deprecate])
|
|
50
53
|
|
|
51
54
|
case type
|
|
52
55
|
when NilClass, FalseClass # Do nothing
|
|
53
56
|
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)
|
|
57
|
+
when /log/ then task.logger.warn { "DEPRECATED: migrate to a replacement or discontinue use" }
|
|
58
|
+
when /warn/ then warn("[#{task.class.name}] DEPRECATED: migrate to a replacement or discontinue use", category: :deprecated)
|
|
56
59
|
else raise "unknown deprecation type #{type.inspect}"
|
|
57
60
|
end
|
|
58
61
|
end
|
data/lib/cmdx/errors.rb
CHANGED
|
@@ -8,11 +8,21 @@ module CMDx
|
|
|
8
8
|
|
|
9
9
|
extend Forwardable
|
|
10
10
|
|
|
11
|
+
# Returns the internal hash of error messages by attribute.
|
|
12
|
+
#
|
|
13
|
+
# @return [Hash{Symbol => Set<String>}] Hash mapping attribute names to error message sets
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# errors.messages # => { email: #<Set: ["must be valid", "is required"]> }
|
|
17
|
+
#
|
|
18
|
+
# @rbs @messages: Hash[Symbol, Set[String]]
|
|
11
19
|
attr_reader :messages
|
|
12
20
|
|
|
13
21
|
def_delegators :messages, :empty?
|
|
14
22
|
|
|
15
23
|
# Initialize a new error collection.
|
|
24
|
+
#
|
|
25
|
+
# @rbs () -> void
|
|
16
26
|
def initialize
|
|
17
27
|
@messages = {}
|
|
18
28
|
end
|
|
@@ -26,6 +36,8 @@ module CMDx
|
|
|
26
36
|
# errors = CMDx::Errors.new
|
|
27
37
|
# errors.add(:email, "must be valid format")
|
|
28
38
|
# errors.add(:email, "cannot be blank")
|
|
39
|
+
#
|
|
40
|
+
# @rbs (Symbol attribute, String message) -> void
|
|
29
41
|
def add(attribute, message)
|
|
30
42
|
return if message.empty?
|
|
31
43
|
|
|
@@ -42,6 +54,8 @@ module CMDx
|
|
|
42
54
|
# @example
|
|
43
55
|
# errors.for?(:email) # => true
|
|
44
56
|
# errors.for?(:name) # => false
|
|
57
|
+
#
|
|
58
|
+
# @rbs (Symbol attribute) -> bool
|
|
45
59
|
def for?(attribute)
|
|
46
60
|
return false unless messages.key?(attribute)
|
|
47
61
|
|
|
@@ -54,6 +68,8 @@ module CMDx
|
|
|
54
68
|
#
|
|
55
69
|
# @example
|
|
56
70
|
# errors.full_messages # => { email: ["email must be valid format", "email cannot be blank"] }
|
|
71
|
+
#
|
|
72
|
+
# @rbs () -> Hash[Symbol, Array[String]]
|
|
57
73
|
def full_messages
|
|
58
74
|
messages.each_with_object({}) do |(attribute, messages), hash|
|
|
59
75
|
hash[attribute] = messages.map { |message| "#{attribute} #{message}" }
|
|
@@ -66,6 +82,8 @@ module CMDx
|
|
|
66
82
|
#
|
|
67
83
|
# @example
|
|
68
84
|
# errors.to_h # => { email: ["must be valid format", "cannot be blank"] }
|
|
85
|
+
#
|
|
86
|
+
# @rbs () -> Hash[Symbol, Array[String]]
|
|
69
87
|
def to_h
|
|
70
88
|
messages.transform_values(&:to_a)
|
|
71
89
|
end
|
|
@@ -78,6 +96,8 @@ module CMDx
|
|
|
78
96
|
# @example
|
|
79
97
|
# errors.to_hash # => { email: ["must be valid format", "cannot be blank"] }
|
|
80
98
|
# errors.to_hash(true) # => { email: ["email must be valid format", "email cannot be blank"] }
|
|
99
|
+
#
|
|
100
|
+
# @rbs (?bool full) -> Hash[Symbol, Array[String]]
|
|
81
101
|
def to_hash(full = false)
|
|
82
102
|
full ? full_messages : to_h
|
|
83
103
|
end
|
|
@@ -88,6 +108,8 @@ module CMDx
|
|
|
88
108
|
#
|
|
89
109
|
# @example
|
|
90
110
|
# errors.to_s # => "email must be valid format. email cannot be blank"
|
|
111
|
+
#
|
|
112
|
+
# @rbs () -> String
|
|
91
113
|
def to_s
|
|
92
114
|
full_messages.values.flatten.join(". ")
|
|
93
115
|
end
|
data/lib/cmdx/executor.rb
CHANGED
|
@@ -8,6 +8,14 @@ module CMDx
|
|
|
8
8
|
# and proper error handling for different types of failures.
|
|
9
9
|
class Executor
|
|
10
10
|
|
|
11
|
+
# Returns the task being executed.
|
|
12
|
+
#
|
|
13
|
+
# @return [Task] The task instance
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# executor.task.id # => "abc123"
|
|
17
|
+
#
|
|
18
|
+
# @rbs @task: Task
|
|
11
19
|
attr_reader :task
|
|
12
20
|
|
|
13
21
|
# @param task [CMDx::Task] The task to execute
|
|
@@ -16,6 +24,8 @@ module CMDx
|
|
|
16
24
|
#
|
|
17
25
|
# @example
|
|
18
26
|
# executor = CMDx::Executor.new(my_task)
|
|
27
|
+
#
|
|
28
|
+
# @rbs (Task task) -> void
|
|
19
29
|
def initialize(task)
|
|
20
30
|
@task = task
|
|
21
31
|
end
|
|
@@ -32,6 +42,8 @@ module CMDx
|
|
|
32
42
|
# @example
|
|
33
43
|
# CMDx::Executor.execute(my_task)
|
|
34
44
|
# CMDx::Executor.execute(my_task, raise: true)
|
|
45
|
+
#
|
|
46
|
+
# @rbs (Task task, raise: bool) -> Result
|
|
35
47
|
def self.execute(task, raise: false)
|
|
36
48
|
instance = new(task)
|
|
37
49
|
raise ? instance.execute! : instance.execute
|
|
@@ -44,16 +56,20 @@ module CMDx
|
|
|
44
56
|
# @example
|
|
45
57
|
# executor = CMDx::Executor.new(my_task)
|
|
46
58
|
# result = executor.execute
|
|
59
|
+
#
|
|
60
|
+
# @rbs () -> Result
|
|
47
61
|
def execute
|
|
48
62
|
task.class.settings[:middlewares].call!(task) do
|
|
49
|
-
pre_execution!
|
|
63
|
+
pre_execution! unless @pre_execution
|
|
50
64
|
execution!
|
|
51
65
|
rescue UndefinedMethodError => e
|
|
52
66
|
raise(e) # No need to clear the Chain since exception is not being re-raised
|
|
53
67
|
rescue Fault => e
|
|
54
68
|
task.result.throw!(e.result, halt: false, cause: e)
|
|
55
69
|
rescue StandardError => e
|
|
70
|
+
retry if retry_execution?(e)
|
|
56
71
|
task.result.fail!("[#{e.class}] #{e.message}", halt: false, cause: e)
|
|
72
|
+
task.class.settings[:exception_handler]&.call(task, e)
|
|
57
73
|
ensure
|
|
58
74
|
task.result.executed!
|
|
59
75
|
post_execution!
|
|
@@ -71,9 +87,11 @@ module CMDx
|
|
|
71
87
|
# @example
|
|
72
88
|
# executor = CMDx::Executor.new(my_task)
|
|
73
89
|
# result = executor.execute!
|
|
90
|
+
#
|
|
91
|
+
# @rbs () -> Result
|
|
74
92
|
def execute!
|
|
75
93
|
task.class.settings[:middlewares].call!(task) do
|
|
76
|
-
pre_execution!
|
|
94
|
+
pre_execution! unless @pre_execution
|
|
77
95
|
execution!
|
|
78
96
|
rescue UndefinedMethodError => e
|
|
79
97
|
raise_exception(e)
|
|
@@ -81,6 +99,7 @@ module CMDx
|
|
|
81
99
|
task.result.throw!(e.result, halt: false, cause: e)
|
|
82
100
|
halt_execution?(e) ? raise_exception(e) : post_execution!
|
|
83
101
|
rescue StandardError => e
|
|
102
|
+
retry if retry_execution?(e)
|
|
84
103
|
task.result.fail!("[#{e.class}] #{e.message}", halt: false, cause: e)
|
|
85
104
|
raise_exception(e)
|
|
86
105
|
else
|
|
@@ -101,6 +120,8 @@ module CMDx
|
|
|
101
120
|
#
|
|
102
121
|
# @example
|
|
103
122
|
# halt_execution?(fault_exception)
|
|
123
|
+
#
|
|
124
|
+
# @rbs (Exception exception) -> bool
|
|
104
125
|
def halt_execution?(exception)
|
|
105
126
|
breakpoints = task.class.settings[:breakpoints] || task.class.settings[:task_breakpoints]
|
|
106
127
|
breakpoints = Array(breakpoints).map(&:to_s).uniq
|
|
@@ -108,6 +129,40 @@ module CMDx
|
|
|
108
129
|
breakpoints.include?(exception.result.status)
|
|
109
130
|
end
|
|
110
131
|
|
|
132
|
+
# Determines if execution should be retried based on retry configuration.
|
|
133
|
+
#
|
|
134
|
+
# @param exception [Exception] The exception that occurred
|
|
135
|
+
#
|
|
136
|
+
# @return [Boolean] Whether execution should be retried
|
|
137
|
+
#
|
|
138
|
+
# @example
|
|
139
|
+
# retry_execution?(standard_error)
|
|
140
|
+
#
|
|
141
|
+
# @rbs (Exception exception) -> bool
|
|
142
|
+
def retry_execution?(exception)
|
|
143
|
+
available_retries = (task.class.settings[:retries] || 0).to_i
|
|
144
|
+
return false unless available_retries.positive?
|
|
145
|
+
|
|
146
|
+
current_retries = (task.result.metadata[:retries] ||= 0).to_i
|
|
147
|
+
remaining_retries = available_retries - current_retries
|
|
148
|
+
return false unless remaining_retries.positive?
|
|
149
|
+
|
|
150
|
+
exceptions = Array(task.class.settings[:retry_on] || StandardError)
|
|
151
|
+
return false unless exceptions.any? { |e| exception.class <= e }
|
|
152
|
+
|
|
153
|
+
task.result.metadata[:retries] += 1
|
|
154
|
+
|
|
155
|
+
task.logger.warn do
|
|
156
|
+
reason = "[#{exception.class}] #{exception.message}"
|
|
157
|
+
task.to_h.merge!(reason:, remaining_retries:)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
jitter = task.class.settings[:retry_jitter].to_f * current_retries
|
|
161
|
+
sleep(jitter) if jitter.positive?
|
|
162
|
+
|
|
163
|
+
true
|
|
164
|
+
end
|
|
165
|
+
|
|
111
166
|
# Raises an exception and clears the chain.
|
|
112
167
|
#
|
|
113
168
|
# @param exception [Exception] The exception to raise
|
|
@@ -116,8 +171,11 @@ module CMDx
|
|
|
116
171
|
#
|
|
117
172
|
# @example
|
|
118
173
|
# raise_exception(standard_error)
|
|
174
|
+
#
|
|
175
|
+
# @rbs (Exception exception) -> void
|
|
119
176
|
def raise_exception(exception)
|
|
120
177
|
Chain.clear
|
|
178
|
+
|
|
121
179
|
raise(exception)
|
|
122
180
|
end
|
|
123
181
|
|
|
@@ -129,14 +187,27 @@ module CMDx
|
|
|
129
187
|
#
|
|
130
188
|
# @example
|
|
131
189
|
# invoke_callbacks(:before_execution)
|
|
190
|
+
#
|
|
191
|
+
# @rbs (Symbol type) -> void
|
|
132
192
|
def invoke_callbacks(type)
|
|
133
193
|
task.class.settings[:callbacks].invoke(type, task)
|
|
134
194
|
end
|
|
135
195
|
|
|
136
196
|
private
|
|
137
197
|
|
|
198
|
+
# Lazy loaded repeator instance to handle retries.
|
|
199
|
+
#
|
|
200
|
+
# @rbs () -> untyped
|
|
201
|
+
def repeator
|
|
202
|
+
@repeator ||= Repeator.new(task)
|
|
203
|
+
end
|
|
204
|
+
|
|
138
205
|
# Performs pre-execution tasks including validation and attribute verification.
|
|
206
|
+
#
|
|
207
|
+
# @rbs () -> void
|
|
139
208
|
def pre_execution!
|
|
209
|
+
@pre_execution = true
|
|
210
|
+
|
|
140
211
|
invoke_callbacks(:before_validation)
|
|
141
212
|
|
|
142
213
|
task.class.settings[:attributes].define_and_verify(task)
|
|
@@ -152,6 +223,8 @@ module CMDx
|
|
|
152
223
|
end
|
|
153
224
|
|
|
154
225
|
# Executes the main task logic.
|
|
226
|
+
#
|
|
227
|
+
# @rbs () -> void
|
|
155
228
|
def execution!
|
|
156
229
|
invoke_callbacks(:before_execution)
|
|
157
230
|
|
|
@@ -160,6 +233,8 @@ module CMDx
|
|
|
160
233
|
end
|
|
161
234
|
|
|
162
235
|
# Performs post-execution tasks including callback invocation.
|
|
236
|
+
#
|
|
237
|
+
# @rbs () -> void
|
|
163
238
|
def post_execution!
|
|
164
239
|
invoke_callbacks(:"on_#{task.result.state}")
|
|
165
240
|
invoke_callbacks(:on_executed) if task.result.executed?
|
|
@@ -170,14 +245,68 @@ module CMDx
|
|
|
170
245
|
end
|
|
171
246
|
|
|
172
247
|
# Finalizes execution by freezing the task and logging results.
|
|
248
|
+
#
|
|
249
|
+
# @rbs () -> Result
|
|
173
250
|
def finalize_execution!
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
251
|
+
log_execution!
|
|
252
|
+
log_backtrace! if task.class.settings[:backtrace]
|
|
253
|
+
|
|
254
|
+
freeze_execution!
|
|
255
|
+
clear_chain!
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Logs the execution result at the configured log level.
|
|
259
|
+
#
|
|
260
|
+
# @rbs () -> void
|
|
261
|
+
def log_execution!
|
|
262
|
+
task.logger.info { task.result.to_h }
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Logs the backtrace of the exception if the task failed.
|
|
266
|
+
#
|
|
267
|
+
# @rbs () -> void
|
|
268
|
+
def log_backtrace!
|
|
269
|
+
return unless task.result.failed?
|
|
270
|
+
|
|
271
|
+
exception = task.result.caused_failure.cause
|
|
272
|
+
return if exception.is_a?(Fault)
|
|
273
|
+
|
|
274
|
+
task.logger.error do
|
|
275
|
+
"[#{exception.class}] #{exception.message}\n" <<
|
|
276
|
+
if (cleaner = task.class.settings[:backtrace_cleaner])
|
|
277
|
+
cleaner.call(exception.backtrace).join("\n\t")
|
|
278
|
+
else
|
|
279
|
+
exception.full_message(highlight: false)
|
|
280
|
+
end
|
|
178
281
|
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Freezes the task and its associated objects to prevent modifications.
|
|
285
|
+
#
|
|
286
|
+
# @rbs () -> void
|
|
287
|
+
def freeze_execution!
|
|
288
|
+
# Stubbing on frozen objects is not allowed in most test environments.
|
|
289
|
+
skip_freezing = ENV.fetch("SKIP_CMDX_FREEZING", false)
|
|
290
|
+
return if Coercions::Boolean.call(skip_freezing)
|
|
291
|
+
|
|
292
|
+
task.freeze
|
|
293
|
+
task.result.freeze
|
|
179
294
|
|
|
180
|
-
|
|
295
|
+
# Freezing the context and chain can only be done
|
|
296
|
+
# once the outer-most task has completed.
|
|
297
|
+
return unless task.result.index.zero?
|
|
298
|
+
|
|
299
|
+
task.context.freeze
|
|
300
|
+
task.chain.freeze
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Clears the chain if the task is the outermost (top-level) task.
|
|
304
|
+
#
|
|
305
|
+
# @rbs () -> void
|
|
306
|
+
def clear_chain!
|
|
307
|
+
return unless task.result.index.zero?
|
|
308
|
+
|
|
309
|
+
Chain.clear
|
|
181
310
|
end
|
|
182
311
|
|
|
183
312
|
end
|