cmdx 1.9.0 → 1.10.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/.cursor/prompts/llms.md +3 -13
- data/.cursor/prompts/yardoc.md +1 -0
- data/CHANGELOG.md +16 -0
- data/LLM.md +436 -374
- data/README.md +7 -2
- data/docs/basics/setup.md +17 -0
- data/docs/callbacks.md +1 -1
- data/docs/getting_started.md +22 -2
- data/docs/index.md +13 -1
- data/docs/retries.md +121 -0
- data/docs/tips_and_tricks.md +2 -1
- data/examples/stoplight_circuit_breaker.md +36 -0
- data/lib/cmdx/attribute.rb +82 -1
- data/lib/cmdx/attribute_registry.rb +20 -0
- data/lib/cmdx/attribute_value.rb +25 -0
- data/lib/cmdx/callback_registry.rb +19 -0
- 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 +2 -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 +126 -3
- data/lib/cmdx/context.rb +36 -0
- data/lib/cmdx/deprecator.rb +3 -0
- data/lib/cmdx/errors.rb +22 -0
- data/lib/cmdx/executor.rb +71 -11
- 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 +20 -1
- data/lib/cmdx/railtie.rb +4 -0
- data/lib/cmdx/result.rb +123 -1
- data/lib/cmdx/task.rb +91 -1
- 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 +8 -0
- data/lib/generators/cmdx/locale_generator.rb +0 -1
- data/mkdocs.yml +3 -1
- metadata +3 -1
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
|
|
@@ -45,6 +46,8 @@ module CMDx
|
|
|
45
46
|
# end
|
|
46
47
|
#
|
|
47
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
|
|
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,6 +56,8 @@ 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
63
|
pre_execution! unless @pre_execution
|
|
@@ -73,6 +87,8 @@ module CMDx
|
|
|
73
87
|
# @example
|
|
74
88
|
# executor = CMDx::Executor.new(my_task)
|
|
75
89
|
# result = executor.execute!
|
|
90
|
+
#
|
|
91
|
+
# @rbs () -> Result
|
|
76
92
|
def execute!
|
|
77
93
|
task.class.settings[:middlewares].call!(task) do
|
|
78
94
|
pre_execution! unless @pre_execution
|
|
@@ -102,13 +118,12 @@ module CMDx
|
|
|
102
118
|
#
|
|
103
119
|
# @return [Boolean] Whether execution should halt
|
|
104
120
|
#
|
|
105
|
-
# @
|
|
106
|
-
# halt_execution?(fault_exception)
|
|
121
|
+
# @rbs (Exception exception) -> bool
|
|
107
122
|
def halt_execution?(exception)
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
statuses = task.class.settings[:breakpoints] || task.class.settings[:task_breakpoints]
|
|
124
|
+
statuses = Array(statuses).map(&:to_s).uniq
|
|
110
125
|
|
|
111
|
-
|
|
126
|
+
statuses.include?(exception.result.status)
|
|
112
127
|
end
|
|
113
128
|
|
|
114
129
|
# Determines if execution should be retried based on retry configuration.
|
|
@@ -117,8 +132,7 @@ module CMDx
|
|
|
117
132
|
#
|
|
118
133
|
# @return [Boolean] Whether execution should be retried
|
|
119
134
|
#
|
|
120
|
-
# @
|
|
121
|
-
# retry_execution?(standard_error)
|
|
135
|
+
# @rbs (Exception exception) -> bool
|
|
122
136
|
def retry_execution?(exception)
|
|
123
137
|
available_retries = (task.class.settings[:retries] || 0).to_i
|
|
124
138
|
return false unless available_retries.positive?
|
|
@@ -137,7 +151,18 @@ module CMDx
|
|
|
137
151
|
task.to_h.merge!(reason:, remaining_retries:)
|
|
138
152
|
end
|
|
139
153
|
|
|
140
|
-
jitter = task.class.settings[:retry_jitter]
|
|
154
|
+
jitter = task.class.settings[:retry_jitter]
|
|
155
|
+
jitter =
|
|
156
|
+
if jitter.is_a?(Symbol)
|
|
157
|
+
task.send(jitter, current_retries)
|
|
158
|
+
elsif jitter.is_a?(Proc)
|
|
159
|
+
task.instance_exec(current_retries, &jitter)
|
|
160
|
+
elsif jitter.respond_to?(:call)
|
|
161
|
+
jitter.call(task, current_retries)
|
|
162
|
+
else
|
|
163
|
+
jitter.to_f * current_retries
|
|
164
|
+
end
|
|
165
|
+
|
|
141
166
|
sleep(jitter) if jitter.positive?
|
|
142
167
|
|
|
143
168
|
true
|
|
@@ -149,8 +174,7 @@ module CMDx
|
|
|
149
174
|
#
|
|
150
175
|
# @raise [Exception] The provided exception
|
|
151
176
|
#
|
|
152
|
-
# @
|
|
153
|
-
# raise_exception(standard_error)
|
|
177
|
+
# @rbs (Exception exception) -> void
|
|
154
178
|
def raise_exception(exception)
|
|
155
179
|
Chain.clear
|
|
156
180
|
|
|
@@ -165,6 +189,8 @@ module CMDx
|
|
|
165
189
|
#
|
|
166
190
|
# @example
|
|
167
191
|
# invoke_callbacks(:before_execution)
|
|
192
|
+
#
|
|
193
|
+
# @rbs (Symbol type) -> void
|
|
168
194
|
def invoke_callbacks(type)
|
|
169
195
|
task.class.settings[:callbacks].invoke(type, task)
|
|
170
196
|
end
|
|
@@ -172,11 +198,15 @@ module CMDx
|
|
|
172
198
|
private
|
|
173
199
|
|
|
174
200
|
# Lazy loaded repeator instance to handle retries.
|
|
201
|
+
#
|
|
202
|
+
# @rbs () -> untyped
|
|
175
203
|
def repeator
|
|
176
204
|
@repeator ||= Repeator.new(task)
|
|
177
205
|
end
|
|
178
206
|
|
|
179
207
|
# Performs pre-execution tasks including validation and attribute verification.
|
|
208
|
+
#
|
|
209
|
+
# @rbs () -> void
|
|
180
210
|
def pre_execution!
|
|
181
211
|
@pre_execution = true
|
|
182
212
|
|
|
@@ -195,6 +225,8 @@ module CMDx
|
|
|
195
225
|
end
|
|
196
226
|
|
|
197
227
|
# Executes the main task logic.
|
|
228
|
+
#
|
|
229
|
+
# @rbs () -> void
|
|
198
230
|
def execution!
|
|
199
231
|
invoke_callbacks(:before_execution)
|
|
200
232
|
|
|
@@ -203,6 +235,8 @@ module CMDx
|
|
|
203
235
|
end
|
|
204
236
|
|
|
205
237
|
# Performs post-execution tasks including callback invocation.
|
|
238
|
+
#
|
|
239
|
+
# @rbs () -> void
|
|
206
240
|
def post_execution!
|
|
207
241
|
invoke_callbacks(:"on_#{task.result.state}")
|
|
208
242
|
invoke_callbacks(:on_executed) if task.result.executed?
|
|
@@ -212,21 +246,29 @@ module CMDx
|
|
|
212
246
|
invoke_callbacks(:on_bad) if task.result.bad?
|
|
213
247
|
end
|
|
214
248
|
|
|
215
|
-
# Finalizes execution by freezing the task and
|
|
249
|
+
# Finalizes execution by freezing the task, logging results, and rolling back work.
|
|
250
|
+
#
|
|
251
|
+
# @rbs () -> Result
|
|
216
252
|
def finalize_execution!
|
|
217
253
|
log_execution!
|
|
218
254
|
log_backtrace! if task.class.settings[:backtrace]
|
|
219
255
|
|
|
220
256
|
freeze_execution!
|
|
221
257
|
clear_chain!
|
|
258
|
+
|
|
259
|
+
rollback_execution!
|
|
222
260
|
end
|
|
223
261
|
|
|
224
262
|
# Logs the execution result at the configured log level.
|
|
263
|
+
#
|
|
264
|
+
# @rbs () -> void
|
|
225
265
|
def log_execution!
|
|
226
266
|
task.logger.info { task.result.to_h }
|
|
227
267
|
end
|
|
228
268
|
|
|
229
269
|
# Logs the backtrace of the exception if the task failed.
|
|
270
|
+
#
|
|
271
|
+
# @rbs () -> void
|
|
230
272
|
def log_backtrace!
|
|
231
273
|
return unless task.result.failed?
|
|
232
274
|
|
|
@@ -244,6 +286,8 @@ module CMDx
|
|
|
244
286
|
end
|
|
245
287
|
|
|
246
288
|
# Freezes the task and its associated objects to prevent modifications.
|
|
289
|
+
#
|
|
290
|
+
# @rbs () -> void
|
|
247
291
|
def freeze_execution!
|
|
248
292
|
# Stubbing on frozen objects is not allowed in most test environments.
|
|
249
293
|
skip_freezing = ENV.fetch("SKIP_CMDX_FREEZING", false)
|
|
@@ -260,11 +304,27 @@ module CMDx
|
|
|
260
304
|
task.chain.freeze
|
|
261
305
|
end
|
|
262
306
|
|
|
307
|
+
# Clears the chain if the task is the outermost (top-level) task.
|
|
308
|
+
#
|
|
309
|
+
# @rbs () -> void
|
|
263
310
|
def clear_chain!
|
|
264
311
|
return unless task.result.index.zero?
|
|
265
312
|
|
|
266
313
|
Chain.clear
|
|
267
314
|
end
|
|
268
315
|
|
|
316
|
+
# Rolls back the work of a task.
|
|
317
|
+
#
|
|
318
|
+
# @rbs () -> void
|
|
319
|
+
def rollback_execution!
|
|
320
|
+
return unless task.respond_to?(:rollback)
|
|
321
|
+
|
|
322
|
+
statuses = task.class.settings[:rollback_on]
|
|
323
|
+
statuses = Array(statuses).map(&:to_s).uniq
|
|
324
|
+
return unless statuses.include?(task.result.status)
|
|
325
|
+
|
|
326
|
+
task.rollback
|
|
327
|
+
end
|
|
328
|
+
|
|
269
329
|
end
|
|
270
330
|
end
|
data/lib/cmdx/faults.rb
CHANGED
|
@@ -11,6 +11,14 @@ module CMDx
|
|
|
11
11
|
|
|
12
12
|
extend Forwardable
|
|
13
13
|
|
|
14
|
+
# Returns the result that caused this fault.
|
|
15
|
+
#
|
|
16
|
+
# @return [Result] The result instance
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# fault.result.reason # => "Validation failed"
|
|
20
|
+
#
|
|
21
|
+
# @rbs @result: Result
|
|
14
22
|
attr_reader :result
|
|
15
23
|
|
|
16
24
|
def_delegators :result, :task, :context, :chain
|
|
@@ -24,6 +32,8 @@ module CMDx
|
|
|
24
32
|
# @example
|
|
25
33
|
# fault = Fault.new(task_result)
|
|
26
34
|
# fault.result.reason # => "Task validation failed"
|
|
35
|
+
#
|
|
36
|
+
# @rbs (Result result) -> void
|
|
27
37
|
def initialize(result)
|
|
28
38
|
@result = result
|
|
29
39
|
|
|
@@ -41,6 +51,8 @@ module CMDx
|
|
|
41
51
|
# @example
|
|
42
52
|
# Fault.for?(UserTask, AdminUserTask)
|
|
43
53
|
# # => true if fault.task is a UserTask or AdminUserTask
|
|
54
|
+
#
|
|
55
|
+
# @rbs (*Class tasks) -> Class
|
|
44
56
|
def for?(*tasks)
|
|
45
57
|
temp_fault = Class.new(self) do
|
|
46
58
|
def self.===(other)
|
|
@@ -62,6 +74,8 @@ module CMDx
|
|
|
62
74
|
# @example
|
|
63
75
|
# Fault.matches? { |fault| fault.result.metadata[:critical] }
|
|
64
76
|
# # => true if fault has critical metadata
|
|
77
|
+
#
|
|
78
|
+
# @rbs () { (Fault) -> bool } -> Class
|
|
65
79
|
def matches?(&block)
|
|
66
80
|
raise ArgumentError, "block required" unless block_given?
|
|
67
81
|
|
data/lib/cmdx/identifier.rb
CHANGED
data/lib/cmdx/locale.rb
CHANGED
|
@@ -8,6 +8,7 @@ module CMDx
|
|
|
8
8
|
|
|
9
9
|
extend self
|
|
10
10
|
|
|
11
|
+
# @rbs EN: Hash[String, untyped]
|
|
11
12
|
EN = YAML.load_file(CMDx.gem_path.join("lib/locales/en.yml")).freeze
|
|
12
13
|
private_constant :EN
|
|
13
14
|
|
|
@@ -34,6 +35,8 @@ module CMDx
|
|
|
34
35
|
# @example With fallback
|
|
35
36
|
# Locale.translate("missing.key", default: "Custom fallback message")
|
|
36
37
|
# # => "Custom fallback message"
|
|
38
|
+
#
|
|
39
|
+
# @rbs ((String | Symbol) key, **untyped options) -> String
|
|
37
40
|
def translate(key, **options)
|
|
38
41
|
options[:default] ||= EN.dig("en", *key.to_s.split("."))
|
|
39
42
|
return ::I18n.t(key, **options) if defined?(::I18n)
|
|
@@ -21,6 +21,8 @@ module CMDx
|
|
|
21
21
|
# @example Basic usage
|
|
22
22
|
# logger_formatter.call("INFO", Time.now, "MyApp", "User logged in")
|
|
23
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
|
|
24
26
|
def call(severity, time, progname, message)
|
|
25
27
|
hash = {
|
|
26
28
|
severity:,
|
|
@@ -21,6 +21,8 @@ module CMDx
|
|
|
21
21
|
# @example Basic usage
|
|
22
22
|
# logger_formatter.call("INFO", Time.now, "MyApp", "User logged in")
|
|
23
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
|
|
24
26
|
def call(severity, time, progname, message)
|
|
25
27
|
hash = {
|
|
26
28
|
severity:,
|
|
@@ -21,6 +21,8 @@ module CMDx
|
|
|
21
21
|
# @example Basic usage
|
|
22
22
|
# logger_formatter.call("INFO", Time.now, "MyApp", "User logged in")
|
|
23
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
|
|
24
26
|
def call(severity, time, progname, message)
|
|
25
27
|
"#{severity[0]}, [#{time.utc.iso8601(6)} ##{Process.pid}] #{severity} -- #{progname}: #{message}\n"
|
|
26
28
|
end
|
|
@@ -22,6 +22,8 @@ module CMDx
|
|
|
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
|
+
#
|
|
26
|
+
# @rbs (String severity, Time time, String? progname, String message) -> String
|
|
25
27
|
def call(severity, time, progname, message)
|
|
26
28
|
hash = {
|
|
27
29
|
severity:,
|
|
@@ -22,6 +22,8 @@ module CMDx
|
|
|
22
22
|
# @example Basic usage
|
|
23
23
|
# logger_formatter.call("INFO", Time.now, "MyApp", "User logged in")
|
|
24
24
|
# # => "User logged in\n"
|
|
25
|
+
#
|
|
26
|
+
# @rbs (String severity, Time time, String? progname, String message) -> String
|
|
25
27
|
def call(severity, time, progname, message)
|
|
26
28
|
"#{message}\n"
|
|
27
29
|
end
|
|
@@ -9,6 +9,14 @@ module CMDx
|
|
|
9
9
|
# they were registered.
|
|
10
10
|
class MiddlewareRegistry
|
|
11
11
|
|
|
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]]
|
|
12
20
|
attr_reader :registry
|
|
13
21
|
alias to_a registry
|
|
14
22
|
|
|
@@ -19,6 +27,8 @@ module CMDx
|
|
|
19
27
|
# @example
|
|
20
28
|
# registry = MiddlewareRegistry.new
|
|
21
29
|
# registry = MiddlewareRegistry.new([[MyMiddleware, {option: 'value'}]])
|
|
30
|
+
#
|
|
31
|
+
# @rbs (?Array[Array[untyped]] registry) -> void
|
|
22
32
|
def initialize(registry = [])
|
|
23
33
|
@registry = registry
|
|
24
34
|
end
|
|
@@ -29,6 +39,8 @@ module CMDx
|
|
|
29
39
|
#
|
|
30
40
|
# @example
|
|
31
41
|
# new_registry = registry.dup
|
|
42
|
+
#
|
|
43
|
+
# @rbs () -> MiddlewareRegistry
|
|
32
44
|
def dup
|
|
33
45
|
self.class.new(registry.map(&:dup))
|
|
34
46
|
end
|
|
@@ -46,6 +58,8 @@ module CMDx
|
|
|
46
58
|
# @example
|
|
47
59
|
# registry.register(LoggingMiddleware, at: 0, log_level: :debug)
|
|
48
60
|
# registry.register(AuthMiddleware, at: -1, timeout: 30)
|
|
61
|
+
#
|
|
62
|
+
# @rbs (untyped middleware, ?at: Integer, **untyped options) -> self
|
|
49
63
|
def register(middleware, at: -1, **options)
|
|
50
64
|
registry.insert(at, [middleware, options])
|
|
51
65
|
self
|
|
@@ -59,6 +73,8 @@ module CMDx
|
|
|
59
73
|
#
|
|
60
74
|
# @example
|
|
61
75
|
# registry.deregister(LoggingMiddleware)
|
|
76
|
+
#
|
|
77
|
+
# @rbs (untyped middleware) -> self
|
|
62
78
|
def deregister(middleware)
|
|
63
79
|
registry.reject! { |mw, _opts| mw == middleware }
|
|
64
80
|
self
|
|
@@ -79,6 +95,8 @@ module CMDx
|
|
|
79
95
|
# result = registry.call!(my_task) do |processed_task|
|
|
80
96
|
# processed_task.execute
|
|
81
97
|
# end
|
|
98
|
+
#
|
|
99
|
+
# @rbs (untyped task) { (untyped) -> untyped } -> untyped
|
|
82
100
|
def call!(task, &)
|
|
83
101
|
raise ArgumentError, "block required" unless block_given?
|
|
84
102
|
|
|
@@ -96,6 +114,8 @@ module CMDx
|
|
|
96
114
|
# @yieldparam task [Object] The processed task object
|
|
97
115
|
#
|
|
98
116
|
# @return [Object] Result of the block execution or next middleware call
|
|
117
|
+
#
|
|
118
|
+
# @rbs (Integer index, untyped task) { (untyped) -> untyped } -> untyped
|
|
99
119
|
def recursively_call_middleware(index, task, &block)
|
|
100
120
|
return yield(task) if index >= registry.size
|
|
101
121
|
|
|
@@ -12,6 +12,7 @@ module CMDx
|
|
|
12
12
|
|
|
13
13
|
extend self
|
|
14
14
|
|
|
15
|
+
# @rbs THREAD_KEY: Symbol
|
|
15
16
|
THREAD_KEY = :cmdx_correlate
|
|
16
17
|
|
|
17
18
|
# Retrieves the current correlation ID from thread-local storage.
|
|
@@ -20,6 +21,8 @@ module CMDx
|
|
|
20
21
|
#
|
|
21
22
|
# @example Get current correlation ID
|
|
22
23
|
# Correlate.id # => "550e8400-e29b-41d4-a716-446655440000"
|
|
24
|
+
#
|
|
25
|
+
# @rbs () -> String?
|
|
23
26
|
def id
|
|
24
27
|
Thread.current[THREAD_KEY]
|
|
25
28
|
end
|
|
@@ -31,6 +34,8 @@ module CMDx
|
|
|
31
34
|
#
|
|
32
35
|
# @example Set correlation ID
|
|
33
36
|
# Correlate.id = "abc-123-def"
|
|
37
|
+
#
|
|
38
|
+
# @rbs (String id) -> String
|
|
34
39
|
def id=(id)
|
|
35
40
|
Thread.current[THREAD_KEY] = id
|
|
36
41
|
end
|
|
@@ -41,6 +46,8 @@ module CMDx
|
|
|
41
46
|
#
|
|
42
47
|
# @example Clear correlation ID
|
|
43
48
|
# Correlate.clear
|
|
49
|
+
#
|
|
50
|
+
# @rbs () -> nil
|
|
44
51
|
def clear
|
|
45
52
|
Thread.current[THREAD_KEY] = nil
|
|
46
53
|
end
|
|
@@ -58,6 +65,8 @@ module CMDx
|
|
|
58
65
|
# perform_operation
|
|
59
66
|
# end
|
|
60
67
|
# # Previous ID is restored
|
|
68
|
+
#
|
|
69
|
+
# @rbs (String new_id) { () -> untyped } -> untyped
|
|
61
70
|
def use(new_id)
|
|
62
71
|
old_id = id
|
|
63
72
|
self.id = new_id
|
|
@@ -92,6 +101,8 @@ module CMDx
|
|
|
92
101
|
# Correlate.call(task, id: -> { "dynamic-#{Time.now.to_i}" }, &block)
|
|
93
102
|
# @example Conditional correlation
|
|
94
103
|
# Correlate.call(task, if: :enable_correlation, &block)
|
|
104
|
+
#
|
|
105
|
+
# @rbs (Task task, **untyped options) { () -> untyped } -> untyped
|
|
95
106
|
def call(task, **options, &)
|
|
96
107
|
return yield unless Utils::Condition.evaluate(task, options)
|
|
97
108
|
|
|
@@ -32,6 +32,8 @@ module CMDx
|
|
|
32
32
|
# Runtime.call(task, if: :enable_profiling, &block)
|
|
33
33
|
# @example Disable runtime measurement
|
|
34
34
|
# Runtime.call(task, unless: :skip_profiling, &block)
|
|
35
|
+
#
|
|
36
|
+
# @rbs (Task task, **untyped options) { () -> untyped } -> untyped
|
|
35
37
|
def call(task, **options)
|
|
36
38
|
return yield unless Utils::Condition.evaluate(task, options)
|
|
37
39
|
|
|
@@ -49,6 +51,8 @@ module CMDx
|
|
|
49
51
|
# timing measurements that are not affected by system clock changes.
|
|
50
52
|
#
|
|
51
53
|
# @return [Integer] Current monotonic time in milliseconds
|
|
54
|
+
#
|
|
55
|
+
# @rbs () -> Integer
|
|
52
56
|
def monotonic_time
|
|
53
57
|
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
54
58
|
end
|