cmdx 2.0.0 → 2.1.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/CHANGELOG.md +62 -8
- data/lib/cmdx/callbacks.rb +31 -11
- data/lib/cmdx/chain.rb +29 -10
- data/lib/cmdx/coercions/big_decimal.rb +1 -1
- data/lib/cmdx/coercions/boolean.rb +3 -9
- data/lib/cmdx/coercions/coerce.rb +4 -1
- data/lib/cmdx/coercions/date_time.rb +1 -1
- data/lib/cmdx/coercions/integer.rb +11 -2
- data/lib/cmdx/coercions/symbol.rb +23 -4
- data/lib/cmdx/coercions.rb +25 -10
- data/lib/cmdx/configuration.rb +31 -16
- data/lib/cmdx/context.rb +40 -56
- data/lib/cmdx/deprecation.rb +4 -7
- data/lib/cmdx/deprecators/error.rb +4 -1
- data/lib/cmdx/deprecators.rb +17 -8
- data/lib/cmdx/errors.rb +15 -11
- data/lib/cmdx/executors/fiber.rb +16 -4
- data/lib/cmdx/executors/thread.rb +18 -4
- data/lib/cmdx/executors.rb +22 -7
- data/lib/cmdx/fault.rb +15 -3
- data/lib/cmdx/i18n_proxy.rb +10 -6
- data/lib/cmdx/input.rb +23 -21
- data/lib/cmdx/inputs.rb +14 -26
- data/lib/cmdx/log_formatters/json.rb +8 -1
- data/lib/cmdx/log_formatters/logstash.rb +7 -1
- data/lib/cmdx/mergers.rb +22 -7
- data/lib/cmdx/middlewares.rb +40 -24
- data/lib/cmdx/output.rb +5 -2
- data/lib/cmdx/pipeline.rb +28 -11
- data/lib/cmdx/railtie.rb +1 -0
- data/lib/cmdx/result.rb +22 -6
- data/lib/cmdx/retriers/decorrelated_jitter.rb +10 -5
- data/lib/cmdx/retriers/exponential.rb +10 -2
- data/lib/cmdx/retriers/fibonacci.rb +29 -12
- data/lib/cmdx/retriers.rb +17 -8
- data/lib/cmdx/retry.rb +20 -13
- data/lib/cmdx/runtime.rb +22 -40
- data/lib/cmdx/settings.rb +9 -9
- data/lib/cmdx/signal.rb +1 -1
- data/lib/cmdx/task.rb +90 -45
- data/lib/cmdx/telemetry.rb +52 -11
- data/lib/cmdx/util.rb +50 -4
- data/lib/cmdx/validators/absence.rb +1 -1
- data/lib/cmdx/validators/exclusion.rb +15 -15
- data/lib/cmdx/validators/format.rb +12 -4
- data/lib/cmdx/validators/inclusion.rb +15 -15
- data/lib/cmdx/validators/length.rb +5 -49
- data/lib/cmdx/validators/numeric.rb +5 -49
- data/lib/cmdx/validators/presence.rb +1 -1
- data/lib/cmdx/validators/validate.rb +7 -1
- data/lib/cmdx/validators.rb +21 -9
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/workflow.rb +28 -14
- data/lib/cmdx.rb +24 -0
- data/lib/generators/cmdx/templates/install.rb +96 -33
- data/mkdocs.yml +2 -0
- metadata +1 -1
data/lib/cmdx/workflow.rb
CHANGED
|
@@ -24,10 +24,11 @@ module CMDx
|
|
|
24
24
|
@pipeline ||= []
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
# Declares a task group.
|
|
28
|
-
#
|
|
27
|
+
# Declares a task group. At least one `Task` subclass is required —
|
|
28
|
+
# invoking with no arguments raises `DefinitionError`. Use {.pipeline}
|
|
29
|
+
# to read the existing group list.
|
|
29
30
|
#
|
|
30
|
-
# @param tasks [Array<Class<Task>>]
|
|
31
|
+
# @param tasks [Array<Class<Task>>] one or more `Task` subclasses
|
|
31
32
|
# @param options [Hash{Symbol => Object}]
|
|
32
33
|
# @option options [:sequential, :parallel] :strategy (:sequential)
|
|
33
34
|
# @option options [Integer] :pool_size parallel worker/fiber count
|
|
@@ -55,18 +56,26 @@ module CMDx
|
|
|
55
56
|
# `:parallel` cancels pending tasks (in-flight tasks still finish).
|
|
56
57
|
# @option options [Symbol, Proc, #call] :if
|
|
57
58
|
# @option options [Symbol, Proc, #call] :unless
|
|
58
|
-
# @return [Array<ExecutionGroup>] the full pipeline
|
|
59
|
-
# @raise [DefinitionError] when called with
|
|
59
|
+
# @return [Array<ExecutionGroup>] the full pipeline (with the new group appended)
|
|
60
|
+
# @raise [DefinitionError] when called with no tasks
|
|
60
61
|
# @raise [TypeError] when any element isn't a `Task` subclass
|
|
61
62
|
def tasks(*tasks, **options)
|
|
62
|
-
|
|
63
|
+
if tasks.empty?
|
|
64
|
+
raise DefinitionError, <<~MSG.chomp
|
|
65
|
+
#{name}: cannot declare an empty task group; pass at least one Task subclass.
|
|
66
|
+
See https://drexed.github.io/cmdx/workflows/#declarations
|
|
67
|
+
MSG
|
|
68
|
+
end
|
|
63
69
|
|
|
64
70
|
pipeline << ExecutionGroup.new(
|
|
65
71
|
tasks:
|
|
66
72
|
tasks.map do |task|
|
|
67
73
|
next task if task.is_a?(Class) && (task <= Task)
|
|
68
74
|
|
|
69
|
-
raise TypeError,
|
|
75
|
+
raise TypeError, <<~MSG.chomp
|
|
76
|
+
#{task.inspect} is not a Task subclass.
|
|
77
|
+
See https://drexed.github.io/cmdx/workflows/#declarations
|
|
78
|
+
MSG
|
|
70
79
|
end,
|
|
71
80
|
options:
|
|
72
81
|
)
|
|
@@ -75,16 +84,13 @@ module CMDx
|
|
|
75
84
|
|
|
76
85
|
private
|
|
77
86
|
|
|
78
|
-
# Forbids user-defined `work` on workflows; `Workflow#work` delegates
|
|
79
|
-
# to {Pipeline}.
|
|
80
|
-
#
|
|
81
|
-
# @param method_name [Symbol] hook name reported by Ruby
|
|
82
|
-
# @return [void]
|
|
83
|
-
# @raise [ImplementationError] when a workflow defines `work`
|
|
84
87
|
def method_added(method_name)
|
|
85
88
|
return super unless method_name == :work
|
|
86
89
|
|
|
87
|
-
raise ImplementationError,
|
|
90
|
+
raise ImplementationError, <<~MSG.chomp
|
|
91
|
+
cannot define #{name}##{method_name} in a workflow; #work is auto-generated to delegate to Pipeline.
|
|
92
|
+
See https://drexed.github.io/cmdx/workflows/#declarations
|
|
93
|
+
MSG
|
|
88
94
|
end
|
|
89
95
|
|
|
90
96
|
end
|
|
@@ -95,7 +101,15 @@ module CMDx
|
|
|
95
101
|
# @api private
|
|
96
102
|
# @param base [Class] task class including this mixin
|
|
97
103
|
# @return [void]
|
|
104
|
+
# @raise [ImplementationError] when `base` is not a {Task} subclass
|
|
98
105
|
def self.included(base)
|
|
106
|
+
unless base.is_a?(Class) && base <= Task
|
|
107
|
+
raise ImplementationError, <<~MSG.chomp
|
|
108
|
+
CMDx::Workflow can only be included in a CMDx::Task subclass (got #{base.inspect}).
|
|
109
|
+
See https://drexed.github.io/cmdx/workflows/#declarations
|
|
110
|
+
MSG
|
|
111
|
+
end
|
|
112
|
+
|
|
99
113
|
base.extend(ClassMethods)
|
|
100
114
|
end
|
|
101
115
|
|
data/lib/cmdx.rb
CHANGED
|
@@ -24,6 +24,13 @@ module CMDx
|
|
|
24
24
|
EMPTY_HASH = {}.freeze
|
|
25
25
|
private_constant :EMPTY_HASH
|
|
26
26
|
|
|
27
|
+
# Sentinel object used as a placeholder return value to avoid per-call
|
|
28
|
+
# allocations on hot paths.
|
|
29
|
+
#
|
|
30
|
+
# @api private
|
|
31
|
+
EMPTY_SENTINEL = Object.new.freeze
|
|
32
|
+
private_constant :EMPTY_SENTINEL
|
|
33
|
+
|
|
27
34
|
# Shared empty string constant used as a sentinel default. Intentionally
|
|
28
35
|
# not frozen so callers may treat it as a mutable seed when needed.
|
|
29
36
|
#
|
|
@@ -51,6 +58,11 @@ module CMDx
|
|
|
51
58
|
# before continuing.
|
|
52
59
|
DeprecationError = Class.new(Error)
|
|
53
60
|
|
|
61
|
+
# Raised when a control-flow signal (e.g. `skip!`, `fail!`) is thrown against
|
|
62
|
+
# a task that has already completed and been frozen, making further state
|
|
63
|
+
# transitions impossible.
|
|
64
|
+
FrozenTaskError = Class.new(Error)
|
|
65
|
+
|
|
54
66
|
# Raised when a subclass fails to fulfill an abstract contract — most
|
|
55
67
|
# commonly when {Task} is invoked without overriding `#work`, or when a
|
|
56
68
|
# {Workflow} attempts to define `#work` itself.
|
|
@@ -60,6 +72,18 @@ module CMDx
|
|
|
60
72
|
# yield to `next_link`, which would otherwise silently skip the task body.
|
|
61
73
|
MiddlewareError = Class.new(Error)
|
|
62
74
|
|
|
75
|
+
# Raised by {Context} in strict mode when accessing a key that was never
|
|
76
|
+
# assigned, preventing silent `nil` propagation across task boundaries.
|
|
77
|
+
UnknownAccessorError = Class.new(Error)
|
|
78
|
+
|
|
79
|
+
# Raised when a registry lookup (coercion, validator, middleware, etc.) is
|
|
80
|
+
# performed against a name that has not been registered.
|
|
81
|
+
UnknownEntryError = Class.new(Error)
|
|
82
|
+
|
|
83
|
+
# Raised when the configured locale cannot be resolved to a translation
|
|
84
|
+
# file on the i18n load path.
|
|
85
|
+
UnknownLocaleError = Class.new(Error)
|
|
86
|
+
|
|
63
87
|
end
|
|
64
88
|
|
|
65
89
|
require_relative "cmdx/version"
|
|
@@ -1,40 +1,54 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
CMDx.configure do |config|
|
|
4
|
+
# Full configuration reference:
|
|
5
|
+
# https://drexed.github.io/cmdx/configuration
|
|
6
|
+
|
|
4
7
|
# ===========================================================================
|
|
5
|
-
#
|
|
8
|
+
# Default locale
|
|
9
|
+
# https://drexed.github.io/cmdx/configuration/#default-locale
|
|
6
10
|
# ===========================================================================
|
|
7
|
-
#
|
|
8
|
-
# the I18n gem
|
|
11
|
+
# The language CMDx uses for its built-in messages (validation errors,
|
|
12
|
+
# coercion errors, etc.) when the `I18n` gem isn't around. If `I18n` IS
|
|
13
|
+
# loaded, CMDx follows `I18n.locale` and ignores this setting.
|
|
9
14
|
#
|
|
10
15
|
# config.default_locale = "en"
|
|
11
16
|
|
|
12
17
|
# ===========================================================================
|
|
13
18
|
# Strict context
|
|
19
|
+
# https://drexed.github.io/cmdx/configuration/#strict-context
|
|
14
20
|
# ===========================================================================
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
21
|
+
# Catches typos early. With strict mode on, `context.usr_id` (instead of
|
|
22
|
+
# `context.user_id`) raises `CMDx::UnknownAccessorError` instead of silently
|
|
23
|
+
# returning `nil`. Hash-style reads (`[]`, `fetch`, `dig`, `?` predicates)
|
|
24
|
+
# stay forgiving. Flip it per task with `settings(strict_context: true)`.
|
|
18
25
|
#
|
|
19
26
|
# config.strict_context = true
|
|
20
27
|
|
|
21
28
|
# ===========================================================================
|
|
22
29
|
# Correlation ID (xid)
|
|
30
|
+
# https://drexed.github.io/cmdx/configuration/#correlation-id-xid
|
|
23
31
|
# ===========================================================================
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
32
|
+
# Stamps every task in a run with the same id so you can grep your logs by
|
|
33
|
+
# request. The callable runs ONCE per root execution; every nested task
|
|
34
|
+
# inherits the value. Surfaces as `result.xid`, `result.to_h[:xid]`, and
|
|
35
|
+
# `event.xid` on telemetry events.
|
|
28
36
|
#
|
|
29
37
|
# config.correlation_id = -> { Current.request_id }
|
|
30
38
|
|
|
31
39
|
# ===========================================================================
|
|
32
40
|
# Logging
|
|
41
|
+
# https://drexed.github.io/cmdx/configuration/#logging
|
|
33
42
|
# ===========================================================================
|
|
34
|
-
#
|
|
35
|
-
#
|
|
43
|
+
# Pick where logs go, how they look, and what to hide. In Rails, the Railtie
|
|
44
|
+
# already points `logger` at `Rails.logger` and wires a backtrace cleaner —
|
|
45
|
+
# only override here if you want something different.
|
|
46
|
+
#
|
|
47
|
+
# Built-in formatters (under `CMDx::LogFormatters`):
|
|
48
|
+
# Line (default), JSON, KeyValue, Logstash, Raw
|
|
36
49
|
#
|
|
37
|
-
#
|
|
50
|
+
# `log_exclusions` only strips keys from the LOG LINE — the in-memory
|
|
51
|
+
# `Result` and telemetry payloads stay complete.
|
|
38
52
|
#
|
|
39
53
|
# config.backtrace_cleaner = ->(bt) { Rails.backtrace_cleaner.clean(bt) }
|
|
40
54
|
# config.log_exclusions = [:context]
|
|
@@ -44,8 +58,12 @@ CMDx.configure do |config|
|
|
|
44
58
|
|
|
45
59
|
# ===========================================================================
|
|
46
60
|
# Middlewares
|
|
61
|
+
# https://drexed.github.io/cmdx/configuration/#middlewares
|
|
47
62
|
# ===========================================================================
|
|
48
|
-
# Wrap every task
|
|
63
|
+
# Wrap every task with shared behavior (auth, locale, timing, you name it).
|
|
64
|
+
# A middleware is anything that responds to `call(task) { ... }` and MUST
|
|
65
|
+
# yield (or call `next_link.call` from a proc) — forgetting to do so raises
|
|
66
|
+
# `CMDx::MiddlewareError` so tasks never silently disappear.
|
|
49
67
|
#
|
|
50
68
|
# Example — run each task under the current user's locale:
|
|
51
69
|
#
|
|
@@ -59,9 +77,13 @@ CMDx.configure do |config|
|
|
|
59
77
|
|
|
60
78
|
# ===========================================================================
|
|
61
79
|
# Callbacks
|
|
80
|
+
# https://drexed.github.io/cmdx/configuration/#callbacks
|
|
62
81
|
# ===========================================================================
|
|
63
|
-
#
|
|
64
|
-
#
|
|
82
|
+
# Hook into a task's lifecycle. Each callback receives the task instance.
|
|
83
|
+
#
|
|
84
|
+
# Available events:
|
|
85
|
+
# :before_execution, :before_validation,
|
|
86
|
+
# :around_execution, :after_execution,
|
|
65
87
|
# :on_complete, :on_interrupted,
|
|
66
88
|
# :on_success, :on_skipped, :on_failed,
|
|
67
89
|
# :on_ok, :on_ko
|
|
@@ -72,16 +94,20 @@ CMDx.configure do |config|
|
|
|
72
94
|
|
|
73
95
|
# ===========================================================================
|
|
74
96
|
# Telemetry
|
|
97
|
+
# https://drexed.github.io/cmdx/configuration/#telemetry
|
|
75
98
|
# ===========================================================================
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
# :
|
|
81
|
-
# :
|
|
99
|
+
# A tiny pub/sub bus for runtime events. Subscribe with a callable; nothing
|
|
100
|
+
# fires if nobody's listening, so unused events cost nothing.
|
|
101
|
+
#
|
|
102
|
+
# Events and their extra payload keys:
|
|
103
|
+
# :task_started {}
|
|
104
|
+
# :task_deprecated {}
|
|
105
|
+
# :task_retried { attempt: Integer }
|
|
106
|
+
# :task_rolled_back {}
|
|
107
|
+
# :task_executed { result: CMDx::Result }
|
|
82
108
|
#
|
|
83
109
|
# Every event also carries: event.cid, event.xid, event.tid, event.task,
|
|
84
|
-
# event.type, event.root, event.timestamp.
|
|
110
|
+
# event.type, event.name, event.root, event.payload, event.timestamp.
|
|
85
111
|
#
|
|
86
112
|
# config.telemetry.subscribe(:task_executed, proc do |event|
|
|
87
113
|
# StatsD.timing("cmdx.task", event.payload[:result].duration)
|
|
@@ -89,8 +115,11 @@ CMDx.configure do |config|
|
|
|
89
115
|
|
|
90
116
|
# ===========================================================================
|
|
91
117
|
# Coercions
|
|
118
|
+
# https://drexed.github.io/cmdx/configuration/#coercions
|
|
92
119
|
# ===========================================================================
|
|
93
|
-
#
|
|
120
|
+
# Teach CMDx how to convert raw input into a custom type. The callable gets
|
|
121
|
+
# `(value, **options)` and returns the coerced value (or
|
|
122
|
+
# `CMDx::Coercions::Failure.new("message")` to signal a bad value).
|
|
94
123
|
#
|
|
95
124
|
# config.coercions.register(:currency, proc do |value, **|
|
|
96
125
|
# BigDecimal(value.to_s.gsub(/[^\d.-]/, ""))
|
|
@@ -98,9 +127,11 @@ CMDx.configure do |config|
|
|
|
98
127
|
|
|
99
128
|
# ===========================================================================
|
|
100
129
|
# Validators
|
|
130
|
+
# https://drexed.github.io/cmdx/configuration/#validators
|
|
101
131
|
# ===========================================================================
|
|
102
|
-
#
|
|
103
|
-
#
|
|
132
|
+
# Custom input validators. The callable gets `(value, options)` (options is
|
|
133
|
+
# a positional Hash). Return `CMDx::Validators::Failure.new(message)` to
|
|
134
|
+
# fail — anything else (even `nil`) means the value passed.
|
|
104
135
|
#
|
|
105
136
|
# config.validators.register(:uuid, proc do |value, _options|
|
|
106
137
|
# unless value.to_s.match?(/\A[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\z/i)
|
|
@@ -108,13 +139,43 @@ CMDx.configure do |config|
|
|
|
108
139
|
# end
|
|
109
140
|
# end)
|
|
110
141
|
|
|
142
|
+
# ===========================================================================
|
|
143
|
+
# Retriers
|
|
144
|
+
# https://drexed.github.io/cmdx/retries/
|
|
145
|
+
# ===========================================================================
|
|
146
|
+
# Retriers decide how long to wait between `retry_on` attempts. A callable
|
|
147
|
+
# gets `(attempt, delay, prev_delay)` and returns the next sleep in seconds.
|
|
148
|
+
#
|
|
149
|
+
# Built-ins: :exponential (default), :linear, :fibonacci, :half_random,
|
|
150
|
+
# :full_random, :bounded_random, :decorrelated_jitter.
|
|
151
|
+
#
|
|
152
|
+
# config.retriers.register(:capped_exponential, proc do |attempt, delay, _prev|
|
|
153
|
+
# [delay * (2**(attempt - 1)), 30.0].min
|
|
154
|
+
# end)
|
|
155
|
+
|
|
156
|
+
# ===========================================================================
|
|
157
|
+
# Deprecators
|
|
158
|
+
# https://drexed.github.io/cmdx/deprecation/
|
|
159
|
+
# ===========================================================================
|
|
160
|
+
# Decide what happens when a task with a `deprecation` declaration runs —
|
|
161
|
+
# log a warning, raise, ping your error tracker, whatever you like. The
|
|
162
|
+
# callable receives the task instance.
|
|
163
|
+
#
|
|
164
|
+
# Built-ins: :log, :warn, :error.
|
|
165
|
+
#
|
|
166
|
+
# config.deprecators.register(:notify, proc do |task|
|
|
167
|
+
# Bugsnag.notify("Deprecated task invoked: #{task.class.name}")
|
|
168
|
+
# end)
|
|
169
|
+
|
|
111
170
|
# ===========================================================================
|
|
112
171
|
# Executors
|
|
172
|
+
# https://drexed.github.io/cmdx/workflows/#parallel-execution
|
|
113
173
|
# ===========================================================================
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
174
|
+
# Executors power `:parallel` workflow groups. The callable gets
|
|
175
|
+
# `(jobs:, concurrency:, on_job:)`, runs `on_job.call(job)` for every job,
|
|
176
|
+
# and blocks until every job is done.
|
|
177
|
+
#
|
|
178
|
+
# Built-ins: :threads (default), :fibers.
|
|
118
179
|
#
|
|
119
180
|
# config.executors.register(:ractors, proc do |jobs:, concurrency:, on_job:|
|
|
120
181
|
# jobs.each_slice(concurrency) do |slice|
|
|
@@ -124,10 +185,12 @@ CMDx.configure do |config|
|
|
|
124
185
|
|
|
125
186
|
# ===========================================================================
|
|
126
187
|
# Mergers
|
|
188
|
+
# https://drexed.github.io/cmdx/workflows/#parallel-execution
|
|
127
189
|
# ===========================================================================
|
|
128
|
-
#
|
|
129
|
-
# workflow context.
|
|
130
|
-
#
|
|
190
|
+
# After parallel branches succeed, a merger folds each branch's context back
|
|
191
|
+
# into the workflow context. The callable gets `(workflow_context, result)`.
|
|
192
|
+
#
|
|
193
|
+
# Built-ins: :last_write_wins (default), :deep_merge, :no_merge.
|
|
131
194
|
#
|
|
132
195
|
# config.mergers.register(:whitelist, proc do |workflow_context, result|
|
|
133
196
|
# result.context.to_h.slice(:order_id, :total).each do |key, value|
|
data/mkdocs.yml
CHANGED
|
@@ -189,12 +189,14 @@ nav:
|
|
|
189
189
|
- Tips and Tricks: tips_and_tricks.md
|
|
190
190
|
- Comparison: comparison.md
|
|
191
191
|
- v1 → v2 Migration: v2-migration.md
|
|
192
|
+
- Examples: https://github.com/drexed/cmdx/blob/main/examples/index.md
|
|
192
193
|
- References:
|
|
193
194
|
- API Documentation: https://drexed.github.io/cmdx/api/index.html
|
|
194
195
|
- llms.txt: https://drexed.github.io/cmdx/llms.txt
|
|
195
196
|
- llms-full.txt: https://drexed.github.io/cmdx/llms-full.txt
|
|
196
197
|
- AI Skills: https://github.com/drexed/cmdx/blob/main/skills
|
|
197
198
|
- Ecosystem:
|
|
199
|
+
- cmdx-i18n: https://github.com/drexed/cmdx-i18n
|
|
198
200
|
- cmdx-rspec: https://github.com/drexed/cmdx-rspec
|
|
199
201
|
- Blog: blog/index.md
|
|
200
202
|
|