cmdx 1.20.0 → 2.0.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 +131 -1
- data/README.md +37 -24
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callbacks.rb +179 -0
- data/lib/cmdx/chain.rb +78 -175
- data/lib/cmdx/coercions/array.rb +19 -33
- data/lib/cmdx/coercions/big_decimal.rb +12 -29
- data/lib/cmdx/coercions/boolean.rb +25 -45
- data/lib/cmdx/coercions/coerce.rb +32 -0
- data/lib/cmdx/coercions/complex.rb +12 -27
- data/lib/cmdx/coercions/date.rb +29 -33
- data/lib/cmdx/coercions/date_time.rb +29 -33
- data/lib/cmdx/coercions/float.rb +8 -29
- data/lib/cmdx/coercions/hash.rb +17 -43
- data/lib/cmdx/coercions/integer.rb +8 -32
- data/lib/cmdx/coercions/rational.rb +12 -33
- data/lib/cmdx/coercions/string.rb +6 -24
- data/lib/cmdx/coercions/symbol.rb +12 -26
- data/lib/cmdx/coercions/time.rb +31 -35
- data/lib/cmdx/coercions.rb +174 -0
- data/lib/cmdx/configuration.rb +45 -225
- data/lib/cmdx/context.rb +263 -242
- data/lib/cmdx/deprecation.rb +67 -0
- data/lib/cmdx/deprecators/error.rb +22 -0
- data/lib/cmdx/deprecators/log.rb +22 -0
- data/lib/cmdx/deprecators/warn.rb +21 -0
- data/lib/cmdx/deprecators.rb +101 -0
- data/lib/cmdx/errors.rb +145 -79
- data/lib/cmdx/executors/fiber.rb +42 -0
- data/lib/cmdx/executors/thread.rb +36 -0
- data/lib/cmdx/executors.rb +95 -0
- data/lib/cmdx/fault.rb +85 -78
- data/lib/cmdx/i18n_proxy.rb +104 -0
- data/lib/cmdx/input.rb +294 -0
- data/lib/cmdx/inputs.rb +218 -0
- data/lib/cmdx/log_formatters/json.rb +9 -20
- data/lib/cmdx/log_formatters/key_value.rb +10 -21
- data/lib/cmdx/log_formatters/line.rb +7 -19
- data/lib/cmdx/log_formatters/logstash.rb +8 -21
- data/lib/cmdx/log_formatters/raw.rb +8 -20
- data/lib/cmdx/logger_proxy.rb +30 -0
- data/lib/cmdx/mergers/deep_merge.rb +23 -0
- data/lib/cmdx/mergers/last_write_wins.rb +23 -0
- data/lib/cmdx/mergers/no_merge.rb +20 -0
- data/lib/cmdx/mergers.rb +95 -0
- data/lib/cmdx/middlewares.rb +128 -0
- data/lib/cmdx/output.rb +115 -0
- data/lib/cmdx/outputs.rb +66 -0
- data/lib/cmdx/pipeline.rb +144 -131
- data/lib/cmdx/railtie.rb +10 -36
- data/lib/cmdx/result.rb +252 -473
- data/lib/cmdx/retriers/bounded_random.rb +24 -0
- data/lib/cmdx/retriers/decorrelated_jitter.rb +28 -0
- data/lib/cmdx/retriers/exponential.rb +23 -0
- data/lib/cmdx/retriers/fibonacci.rb +39 -0
- data/lib/cmdx/retriers/full_random.rb +23 -0
- data/lib/cmdx/retriers/half_random.rb +24 -0
- data/lib/cmdx/retriers/linear.rb +23 -0
- data/lib/cmdx/retriers.rb +106 -0
- data/lib/cmdx/retry.rb +117 -138
- data/lib/cmdx/runtime.rb +251 -0
- data/lib/cmdx/settings.rb +68 -196
- data/lib/cmdx/signal.rb +165 -0
- data/lib/cmdx/task.rb +443 -336
- data/lib/cmdx/telemetry.rb +108 -0
- data/lib/cmdx/util.rb +73 -0
- data/lib/cmdx/validators/absence.rb +10 -39
- data/lib/cmdx/validators/exclusion.rb +33 -52
- data/lib/cmdx/validators/format.rb +19 -49
- data/lib/cmdx/validators/inclusion.rb +33 -54
- data/lib/cmdx/validators/length.rb +125 -127
- data/lib/cmdx/validators/numeric.rb +123 -123
- data/lib/cmdx/validators/presence.rb +10 -39
- data/lib/cmdx/validators/validate.rb +31 -0
- data/lib/cmdx/validators.rb +161 -0
- data/lib/cmdx/version.rb +2 -4
- data/lib/cmdx/workflow.rb +74 -82
- data/lib/cmdx.rb +111 -42
- data/lib/generators/cmdx/install_generator.rb +7 -17
- data/lib/generators/cmdx/task_generator.rb +12 -29
- data/lib/generators/cmdx/templates/install.rb +128 -52
- data/lib/generators/cmdx/templates/task.rb.tt +1 -1
- data/lib/generators/cmdx/templates/workflow.rb.tt +1 -2
- data/lib/generators/cmdx/workflow_generator.rb +12 -29
- data/lib/locales/en.yml +9 -6
- data/mkdocs.yml +25 -23
- metadata +39 -138
- data/lib/cmdx/attribute.rb +0 -440
- data/lib/cmdx/attribute_registry.rb +0 -185
- data/lib/cmdx/attribute_value.rb +0 -252
- data/lib/cmdx/callback_registry.rb +0 -169
- data/lib/cmdx/coercion_registry.rb +0 -138
- data/lib/cmdx/deprecator.rb +0 -77
- data/lib/cmdx/exception.rb +0 -46
- data/lib/cmdx/executor.rb +0 -374
- data/lib/cmdx/identifier.rb +0 -30
- data/lib/cmdx/locale.rb +0 -78
- data/lib/cmdx/middleware_registry.rb +0 -148
- data/lib/cmdx/middlewares/correlate.rb +0 -140
- data/lib/cmdx/middlewares/runtime.rb +0 -62
- data/lib/cmdx/middlewares/timeout.rb +0 -78
- data/lib/cmdx/parallelizer.rb +0 -100
- data/lib/cmdx/utils/call.rb +0 -53
- data/lib/cmdx/utils/condition.rb +0 -71
- data/lib/cmdx/utils/format.rb +0 -82
- data/lib/cmdx/utils/normalize.rb +0 -52
- data/lib/cmdx/utils/wrap.rb +0 -38
- data/lib/cmdx/validator_registry.rb +0 -143
- data/lib/generators/cmdx/locale_generator.rb +0 -39
- data/lib/locales/af.yml +0 -53
- data/lib/locales/ar.yml +0 -53
- data/lib/locales/az.yml +0 -53
- data/lib/locales/be.yml +0 -53
- data/lib/locales/bg.yml +0 -53
- data/lib/locales/bn.yml +0 -53
- data/lib/locales/bs.yml +0 -53
- data/lib/locales/ca.yml +0 -53
- data/lib/locales/cnr.yml +0 -53
- data/lib/locales/cs.yml +0 -53
- data/lib/locales/cy.yml +0 -53
- data/lib/locales/da.yml +0 -53
- data/lib/locales/de.yml +0 -53
- data/lib/locales/dz.yml +0 -53
- data/lib/locales/el.yml +0 -53
- data/lib/locales/eo.yml +0 -53
- data/lib/locales/es.yml +0 -53
- data/lib/locales/et.yml +0 -53
- data/lib/locales/eu.yml +0 -53
- data/lib/locales/fa.yml +0 -53
- data/lib/locales/fi.yml +0 -53
- data/lib/locales/fr.yml +0 -53
- data/lib/locales/fy.yml +0 -53
- data/lib/locales/gd.yml +0 -53
- data/lib/locales/gl.yml +0 -53
- data/lib/locales/he.yml +0 -53
- data/lib/locales/hi.yml +0 -53
- data/lib/locales/hr.yml +0 -53
- data/lib/locales/hu.yml +0 -53
- data/lib/locales/hy.yml +0 -53
- data/lib/locales/id.yml +0 -53
- data/lib/locales/is.yml +0 -53
- data/lib/locales/it.yml +0 -53
- data/lib/locales/ja.yml +0 -53
- data/lib/locales/ka.yml +0 -53
- data/lib/locales/kk.yml +0 -53
- data/lib/locales/km.yml +0 -53
- data/lib/locales/kn.yml +0 -53
- data/lib/locales/ko.yml +0 -53
- data/lib/locales/lb.yml +0 -53
- data/lib/locales/lo.yml +0 -53
- data/lib/locales/lt.yml +0 -53
- data/lib/locales/lv.yml +0 -53
- data/lib/locales/mg.yml +0 -53
- data/lib/locales/mk.yml +0 -53
- data/lib/locales/ml.yml +0 -53
- data/lib/locales/mn.yml +0 -53
- data/lib/locales/mr-IN.yml +0 -53
- data/lib/locales/ms.yml +0 -53
- data/lib/locales/nb.yml +0 -53
- data/lib/locales/ne.yml +0 -53
- data/lib/locales/nl.yml +0 -53
- data/lib/locales/nn.yml +0 -53
- data/lib/locales/oc.yml +0 -53
- data/lib/locales/or.yml +0 -53
- data/lib/locales/pa.yml +0 -53
- data/lib/locales/pl.yml +0 -53
- data/lib/locales/pt.yml +0 -53
- data/lib/locales/rm.yml +0 -53
- data/lib/locales/ro.yml +0 -53
- data/lib/locales/ru.yml +0 -53
- data/lib/locales/sc.yml +0 -53
- data/lib/locales/sk.yml +0 -53
- data/lib/locales/sl.yml +0 -53
- data/lib/locales/sq.yml +0 -53
- data/lib/locales/sr.yml +0 -53
- data/lib/locales/st.yml +0 -53
- data/lib/locales/sv.yml +0 -53
- data/lib/locales/sw.yml +0 -53
- data/lib/locales/ta.yml +0 -53
- data/lib/locales/te.yml +0 -53
- data/lib/locales/th.yml +0 -53
- data/lib/locales/tl.yml +0 -53
- data/lib/locales/tr.yml +0 -53
- data/lib/locales/tt.yml +0 -53
- data/lib/locales/ug.yml +0 -53
- data/lib/locales/uk.yml +0 -53
- data/lib/locales/ur.yml +0 -53
- data/lib/locales/uz.yml +0 -53
- data/lib/locales/vi.yml +0 -53
- data/lib/locales/wo.yml +0 -53
- data/lib/locales/zh-CN.yml +0 -53
- data/lib/locales/zh-HK.yml +0 -53
- data/lib/locales/zh-TW.yml +0 -53
- data/lib/locales/zh-YUE.yml +0 -53
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
# Pub/sub for runtime lifecycle events (see {EVENTS}). Subscribers are
|
|
5
|
+
# callables receiving an {Event} data object. Runtime emits events only when
|
|
6
|
+
# subscribers are registered so telemetry has zero cost when unused.
|
|
7
|
+
class Telemetry
|
|
8
|
+
|
|
9
|
+
# Immutable event payload passed to subscribers.
|
|
10
|
+
Event = Data.define(:xid, :cid, :root, :type, :task, :tid, :name, :payload, :timestamp)
|
|
11
|
+
|
|
12
|
+
# Lifecycle event names Runtime emits.
|
|
13
|
+
EVENTS = %i[
|
|
14
|
+
task_started
|
|
15
|
+
task_deprecated
|
|
16
|
+
task_retried
|
|
17
|
+
task_rolled_back
|
|
18
|
+
task_executed
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :registry
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@registry = {}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param source [Telemetry] registry to duplicate
|
|
28
|
+
# @return [void]
|
|
29
|
+
def initialize_copy(source)
|
|
30
|
+
@registry = source.registry.transform_values(&:dup)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Registers a subscriber for `event`.
|
|
34
|
+
#
|
|
35
|
+
# @param event [Symbol] one of {EVENTS}
|
|
36
|
+
# @param callable [#call, nil] subscriber callable; pass either this or a block
|
|
37
|
+
# @param block [#call, nil] subscriber when `callable` is omitted
|
|
38
|
+
# @yieldparam evt [Event]
|
|
39
|
+
# @return [Telemetry] self for chaining
|
|
40
|
+
# @raise [ArgumentError] when both `callable` and a block are provided, when
|
|
41
|
+
# the subscriber isn't callable, or when `event` is unknown
|
|
42
|
+
def subscribe(event, callable = nil, &block)
|
|
43
|
+
subscriber = callable || block
|
|
44
|
+
|
|
45
|
+
if callable && block
|
|
46
|
+
raise ArgumentError, "provide either a callable or a block, not both"
|
|
47
|
+
elsif !subscriber.respond_to?(:call)
|
|
48
|
+
raise ArgumentError, "subscriber must respond to #call"
|
|
49
|
+
elsif !EVENTS.include?(event)
|
|
50
|
+
raise ArgumentError, "unknown event #{event.inspect}, must be one of #{EVENTS.join(', ')}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
(registry[event] ||= []) << subscriber
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Removes a previously registered subscriber. Drops the event entry
|
|
58
|
+
# entirely when no subscribers remain.
|
|
59
|
+
#
|
|
60
|
+
# @param event [Symbol] one of {EVENTS}
|
|
61
|
+
# @param callable [#call] the subscriber to remove
|
|
62
|
+
# @return [Telemetry] self for chaining
|
|
63
|
+
# @raise [ArgumentError] when `event` is unknown
|
|
64
|
+
def unsubscribe(event, callable)
|
|
65
|
+
raise ArgumentError, "unknown event #{event.inspect}, must be one of #{EVENTS.join(', ')}" unless EVENTS.include?(event)
|
|
66
|
+
|
|
67
|
+
return self unless subscribed?(event)
|
|
68
|
+
|
|
69
|
+
registry[event].delete(callable)
|
|
70
|
+
registry.delete(event) if registry[event].empty?
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @param event [Symbol]
|
|
75
|
+
# @return [Boolean] true when at least one subscriber exists for `event`
|
|
76
|
+
def subscribed?(event)
|
|
77
|
+
registry.key?(event)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @return [Boolean]
|
|
81
|
+
def empty?
|
|
82
|
+
registry.empty?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @return [Integer] number of subscribed events
|
|
86
|
+
def size
|
|
87
|
+
registry.size
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @return [Integer] total subscribers across all events
|
|
91
|
+
def count
|
|
92
|
+
registry.each_value.sum(&:size)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Dispatches `payload` to every subscriber of `event`. No-op when there
|
|
96
|
+
# are no subscribers.
|
|
97
|
+
#
|
|
98
|
+
# @param event [Symbol]
|
|
99
|
+
# @param payload [Event]
|
|
100
|
+
# @return [void]
|
|
101
|
+
def emit(event, payload)
|
|
102
|
+
return unless (subscribers = registry[event])
|
|
103
|
+
|
|
104
|
+
subscribers.each { |s| s.call(payload) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/cmdx/util.rb
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
# Shared helpers for resolving `:if` / `:unless` conditional options across
|
|
5
|
+
# tasks, callbacks, inputs, outputs, validators, and deprecations. Normalizes
|
|
6
|
+
# booleans, symbols (method names), procs, and call-ables into a truth value.
|
|
7
|
+
module Util
|
|
8
|
+
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
# Evaluates a condition against `receiver`, dispatching by type.
|
|
12
|
+
#
|
|
13
|
+
# @param condition [Boolean, nil, Symbol, Proc, #call] `:if`/`:unless`-style gate, method name, or callable evaluated against `receiver`
|
|
14
|
+
# @param receiver [Object] object the condition runs against (usually a Task)
|
|
15
|
+
# @param args [Array<Object>] extra arguments forwarded to the condition
|
|
16
|
+
# @return [Boolean, Object] truthiness result (Procs `instance_exec` on receiver)
|
|
17
|
+
# @raise [ArgumentError] when the condition is not a supported type
|
|
18
|
+
def evaluate(condition, receiver, *args)
|
|
19
|
+
case condition
|
|
20
|
+
when FalseClass, NilClass
|
|
21
|
+
false
|
|
22
|
+
when TrueClass
|
|
23
|
+
true
|
|
24
|
+
when Symbol
|
|
25
|
+
receiver.send(condition, *args)
|
|
26
|
+
when Proc
|
|
27
|
+
receiver.instance_exec(*args, &condition)
|
|
28
|
+
else
|
|
29
|
+
return condition.call(receiver, *args) if condition.respond_to?(:call)
|
|
30
|
+
|
|
31
|
+
raise ArgumentError, "condition must be a Symbol, Proc, or respond to #call"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Evaluates an `:if`-style condition. `nil` is treated as "always true".
|
|
36
|
+
#
|
|
37
|
+
# @param condition [Boolean, nil, Symbol, Proc, #call] gate to check
|
|
38
|
+
# @param receiver [Object] object the condition runs against
|
|
39
|
+
# @param args [Array<Object>] extra arguments forwarded to the condition
|
|
40
|
+
# @return [Boolean] true when `condition` is nil or evaluates truthy
|
|
41
|
+
def if?(condition, receiver, *args)
|
|
42
|
+
return true if condition.nil?
|
|
43
|
+
|
|
44
|
+
evaluate(condition, receiver, *args)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Evaluates an `:unless`-style condition. `nil` is treated as "always true".
|
|
48
|
+
#
|
|
49
|
+
# @param condition [Boolean, nil, Symbol, Proc, #call] gate to check
|
|
50
|
+
# @param receiver [Object] object the condition runs against
|
|
51
|
+
# @param args [Array<Object>] extra arguments forwarded to the condition
|
|
52
|
+
# @return [Boolean] true when `condition` is nil or evaluates falsy
|
|
53
|
+
def unless?(condition, receiver, *args)
|
|
54
|
+
return true if condition.nil?
|
|
55
|
+
|
|
56
|
+
!evaluate(condition, receiver, *args)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Combines `:if` and `:unless` gates. Used across the framework to decide
|
|
60
|
+
# whether a conditional feature (callback, retry, validator, etc.) should run.
|
|
61
|
+
#
|
|
62
|
+
# @param condition_if [Boolean, nil, Symbol, Proc, #call] `:if` gate
|
|
63
|
+
# @param condition_unless [Boolean, nil, Symbol, Proc, #call] `:unless` gate
|
|
64
|
+
# @param receiver [Object] object the conditions run against
|
|
65
|
+
# @param args [Array<Object>] extra arguments forwarded to both conditions
|
|
66
|
+
# @return [Boolean] true only when both gates pass
|
|
67
|
+
def satisfied?(condition_if, condition_unless, receiver, *args)
|
|
68
|
+
if?(condition_if, receiver, *args) &&
|
|
69
|
+
unless?(condition_unless, receiver, *args)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -1,47 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
# Validates that a value is
|
|
6
|
-
#
|
|
7
|
-
# This validator ensures that the given value is nil, empty, or consists only of whitespace.
|
|
8
|
-
# It handles different value types appropriately:
|
|
9
|
-
# - Strings: checks for absence of non-whitespace characters
|
|
10
|
-
# - Collections: checks for empty collections
|
|
11
|
-
# - Other objects: checks for nil values
|
|
4
|
+
class Validators
|
|
5
|
+
# Validates that a value is blank: `nil`, whitespace-only string, or
|
|
6
|
+
# empty collection.
|
|
12
7
|
module Absence
|
|
13
8
|
|
|
14
9
|
extend self
|
|
15
10
|
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# @
|
|
19
|
-
# @
|
|
20
|
-
# @option options [String] :message Custom error message
|
|
21
|
-
#
|
|
22
|
-
# @return [nil] Returns nil if validation passes
|
|
23
|
-
#
|
|
24
|
-
# @raise [ValidationError] When the value is present, not empty, or contains non-whitespace characters
|
|
25
|
-
#
|
|
26
|
-
# @example Validate string absence
|
|
27
|
-
# Absence.call("")
|
|
28
|
-
# # => nil (validation passes)
|
|
29
|
-
# @example Validate non-empty string
|
|
30
|
-
# Absence.call("hello")
|
|
31
|
-
# # => raises ValidationError
|
|
32
|
-
# @example Validate array absence
|
|
33
|
-
# Absence.call([])
|
|
34
|
-
# # => nil (validation passes)
|
|
35
|
-
# @example Validate non-empty array
|
|
36
|
-
# Absence.call([1, 2, 3])
|
|
37
|
-
# # => raises ValidationError
|
|
38
|
-
# @example Validate with custom message
|
|
39
|
-
# Absence.call("hello", message: "Value must be empty")
|
|
40
|
-
# # => raises ValidationError with custom message
|
|
41
|
-
#
|
|
42
|
-
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> nil
|
|
11
|
+
# @param value [Object]
|
|
12
|
+
# @param options [Hash{Symbol => Object}]
|
|
13
|
+
# @option options [String] :message override for the failure message
|
|
14
|
+
# @return [Validators::Failure, nil]
|
|
43
15
|
def call(value, options = EMPTY_HASH)
|
|
44
|
-
|
|
16
|
+
present =
|
|
45
17
|
if value.is_a?(String)
|
|
46
18
|
/\S/.match?(value)
|
|
47
19
|
elsif value.respond_to?(:empty?)
|
|
@@ -50,10 +22,9 @@ module CMDx
|
|
|
50
22
|
!value.nil?
|
|
51
23
|
end
|
|
52
24
|
|
|
53
|
-
return unless
|
|
25
|
+
return unless present
|
|
54
26
|
|
|
55
|
-
|
|
56
|
-
raise ValidationError, message || Locale.t("cmdx.validators.absence")
|
|
27
|
+
Failure.new(options[:message] || I18nProxy.t("cmdx.validators.absence"))
|
|
57
28
|
end
|
|
58
29
|
|
|
59
30
|
end
|
|
@@ -1,79 +1,60 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# This validator ensures that the given value is excluded from a collection
|
|
8
|
-
# of forbidden values or falls outside a specified range. It supports both
|
|
9
|
-
# discrete value lists and range-based exclusions.
|
|
4
|
+
class Validators
|
|
5
|
+
# Inverse of {Inclusion}: the value must not be within the given
|
|
6
|
+
# enumerable or `Range`.
|
|
10
7
|
module Exclusion
|
|
11
8
|
|
|
12
9
|
extend self
|
|
13
10
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# @
|
|
17
|
-
# @
|
|
18
|
-
# @option options [
|
|
19
|
-
# @option options [
|
|
20
|
-
# @option options [String] :
|
|
21
|
-
# @
|
|
22
|
-
# @
|
|
23
|
-
# @option options [String] :within_message Custom message for range-based exclusions
|
|
24
|
-
#
|
|
25
|
-
# @raise [ValidationError] When the value is found in the forbidden collection
|
|
26
|
-
#
|
|
27
|
-
# @example Exclude specific values
|
|
28
|
-
# Exclusion.call("admin", in: ["admin", "root", "superuser"])
|
|
29
|
-
# # => raises ValidationError if value is "admin"
|
|
30
|
-
# @example Exclude values within a range
|
|
31
|
-
# Exclusion.call(5, in: 1..10)
|
|
32
|
-
# # => raises ValidationError if value is 5 (within 1..10)
|
|
33
|
-
# @example Exclude with custom message
|
|
34
|
-
# Exclusion.call("test", in: ["test", "demo"], message: "value %{values} is forbidden")
|
|
35
|
-
#
|
|
36
|
-
# @rbs (untyped value, Hash[Symbol, untyped] options) -> nil
|
|
11
|
+
# @param value [Object]
|
|
12
|
+
# @param options [Hash{Symbol => Object}]
|
|
13
|
+
# @option options [Range, Array, Set, Enumerable] :in disallowed values
|
|
14
|
+
# @option options [Range, Array, Set, Enumerable] :within alias for `:in`
|
|
15
|
+
# @option options [String] :message global failure-message override
|
|
16
|
+
# @option options [String] :of_message override for enumerable failures
|
|
17
|
+
# @option options [String] :in_message, :within_message overrides for range failures
|
|
18
|
+
# @return [Validators::Failure, nil]
|
|
19
|
+
# @raise [ArgumentError] when neither `:in` nor `:within` is given
|
|
37
20
|
def call(value, options = EMPTY_HASH)
|
|
38
21
|
values = options[:in] || options[:within]
|
|
22
|
+
raise ArgumentError, "exclusion validator requires :in or :within option" if values.nil?
|
|
39
23
|
|
|
40
24
|
if values.is_a?(Range)
|
|
41
|
-
|
|
42
|
-
elsif
|
|
43
|
-
|
|
25
|
+
within_failure(values.begin, values.end, options) if values.cover?(value)
|
|
26
|
+
elsif Array(values).any? { |v| v === value }
|
|
27
|
+
of_failure(values, options)
|
|
44
28
|
end
|
|
45
29
|
end
|
|
46
30
|
|
|
47
31
|
private
|
|
48
32
|
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
# @
|
|
52
|
-
# @
|
|
53
|
-
# @
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def raise_of_validation_error!(values, options)
|
|
57
|
-
values = values.map(&:inspect).join(", ") unless values.nil?
|
|
33
|
+
# @param values [Enumerable] collection rendered into the failure message
|
|
34
|
+
# @param options [Hash{Symbol => Object}]
|
|
35
|
+
# @option options [String] :of_message
|
|
36
|
+
# @option options [String] :message
|
|
37
|
+
# @return [Validators::Failure]
|
|
38
|
+
def of_failure(values, options)
|
|
39
|
+
values = values.map(&:inspect).join(", ")
|
|
58
40
|
message = options[:of_message] || options[:message]
|
|
59
41
|
message %= { values: } unless message.nil?
|
|
60
42
|
|
|
61
|
-
|
|
43
|
+
Failure.new(message || I18nProxy.t("cmdx.validators.exclusion.of", values:))
|
|
62
44
|
end
|
|
63
45
|
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
# @param
|
|
67
|
-
# @
|
|
68
|
-
# @
|
|
69
|
-
# @option options [
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
def raise_within_validation_error!(min, max, options)
|
|
46
|
+
# @param min [Object] range/exclusion lower bound
|
|
47
|
+
# @param max [Object] range/exclusion upper bound
|
|
48
|
+
# @param options [Hash{Symbol => Object}]
|
|
49
|
+
# @option options [String] :in_message
|
|
50
|
+
# @option options [String] :within_message
|
|
51
|
+
# @option options [String] :message
|
|
52
|
+
# @return [Validators::Failure]
|
|
53
|
+
def within_failure(min, max, options)
|
|
73
54
|
message = options[:in_message] || options[:within_message] || options[:message]
|
|
74
55
|
message %= { min:, max: } unless message.nil?
|
|
75
56
|
|
|
76
|
-
|
|
57
|
+
Failure.new(message || I18nProxy.t("cmdx.validators.exclusion.within", min:, max:))
|
|
77
58
|
end
|
|
78
59
|
|
|
79
60
|
end
|
|
@@ -1,66 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
# Validates that a value matches a
|
|
6
|
-
#
|
|
7
|
-
# This validator ensures that the given value conforms to a specific format
|
|
8
|
-
# using regular expressions. It supports both direct regex matching and
|
|
9
|
-
# conditional matching with inclusion/exclusion patterns.
|
|
4
|
+
class Validators
|
|
5
|
+
# Validates that a value matches a `:with` regex and/or does not match a
|
|
6
|
+
# `:without` regex. Both may be combined; at least one is required.
|
|
10
7
|
module Format
|
|
11
8
|
|
|
12
9
|
extend self
|
|
13
10
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# @
|
|
17
|
-
# @
|
|
18
|
-
# @option options [
|
|
19
|
-
# @
|
|
20
|
-
# @
|
|
21
|
-
#
|
|
22
|
-
# @return [nil] Returns nil if validation passes
|
|
23
|
-
#
|
|
24
|
-
# @raise [ValidationError] When the value doesn't match the required format
|
|
25
|
-
#
|
|
26
|
-
# @example Direct regex validation
|
|
27
|
-
# Format.call("user@example.com", /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
|
|
28
|
-
# # => nil (validation passes)
|
|
29
|
-
# @example Validate with required pattern
|
|
30
|
-
# Format.call("ABC123", with: /\A[A-Z]{3}\d{3}\z/)
|
|
31
|
-
# # => nil (validation passes)
|
|
32
|
-
# @example Validate with exclusion pattern
|
|
33
|
-
# Format.call("hello", without: /\d/)
|
|
34
|
-
# # => nil (validation passes - no digits)
|
|
35
|
-
# @example Validate with both patterns
|
|
36
|
-
# Format.call("test123", with: /\A\w+\z/, without: /\A\d+\z/)
|
|
37
|
-
# # => nil (validation passes - alphanumeric but not all digits)
|
|
38
|
-
# @example Validate with custom message
|
|
39
|
-
# Format.call("invalid", with: /\A\d+\z/, message: "Must contain only digits")
|
|
40
|
-
# # => raises ValidationError with custom message
|
|
41
|
-
#
|
|
42
|
-
# @rbs (untyped value, (Hash[Symbol, untyped] | Regexp) options) -> nil
|
|
11
|
+
# @param value [String, nil]
|
|
12
|
+
# @param options [Hash{Symbol => Object}]
|
|
13
|
+
# @option options [Regexp] :with must match
|
|
14
|
+
# @option options [Regexp] :without must not match
|
|
15
|
+
# @option options [String] :message override for the failure message
|
|
16
|
+
# @return [Validators::Failure, nil]
|
|
17
|
+
# @raise [ArgumentError] when neither `:with` nor `:without` is given
|
|
43
18
|
def call(value, options = EMPTY_HASH)
|
|
44
19
|
match =
|
|
45
|
-
|
|
46
|
-
|
|
20
|
+
case options
|
|
21
|
+
in with:, without:
|
|
22
|
+
value&.match?(with) && !value&.match?(without)
|
|
23
|
+
in with:
|
|
24
|
+
value&.match?(with)
|
|
25
|
+
in without:
|
|
26
|
+
!value&.match?(without)
|
|
47
27
|
else
|
|
48
|
-
|
|
49
|
-
in with:, without:
|
|
50
|
-
value&.match?(with) && !value&.match?(without)
|
|
51
|
-
in with:
|
|
52
|
-
value&.match?(with)
|
|
53
|
-
in without:
|
|
54
|
-
!value&.match?(without)
|
|
55
|
-
else
|
|
56
|
-
false
|
|
57
|
-
end
|
|
28
|
+
raise ArgumentError, "format validator requires :with and/or :without option"
|
|
58
29
|
end
|
|
59
30
|
|
|
60
31
|
return if match
|
|
61
32
|
|
|
62
|
-
|
|
63
|
-
raise ValidationError, message || Locale.t("cmdx.validators.format")
|
|
33
|
+
Failure.new(options[:message] || I18nProxy.t("cmdx.validators.format"))
|
|
64
34
|
end
|
|
65
35
|
|
|
66
36
|
end
|
|
@@ -1,81 +1,60 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
# Validates that a value is
|
|
6
|
-
#
|
|
7
|
-
# This validator ensures that the given value is present within a collection
|
|
8
|
-
# of allowed values or falls within a specified range. It supports both
|
|
9
|
-
# discrete value lists and range-based validations.
|
|
4
|
+
class Validators
|
|
5
|
+
# Validates that a value is within an enumerable or `Range`. Range uses
|
|
6
|
+
# `#cover?`; other enumerables use `===` (so regex/class matchers work).
|
|
10
7
|
module Inclusion
|
|
11
8
|
|
|
12
9
|
extend self
|
|
13
10
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# @
|
|
17
|
-
# @
|
|
18
|
-
# @option options [
|
|
19
|
-
# @option options [
|
|
20
|
-
# @option options [String] :
|
|
21
|
-
# @
|
|
22
|
-
# @
|
|
23
|
-
# @option options [String] :within_message Custom message for range-based inclusions
|
|
24
|
-
#
|
|
25
|
-
# @return [nil] Returns nil if validation passes
|
|
26
|
-
#
|
|
27
|
-
# @raise [ValidationError] When the value is not found in the allowed collection
|
|
28
|
-
#
|
|
29
|
-
# @example Include specific values
|
|
30
|
-
# Inclusion.call("admin", in: ["admin", "user", "guest"])
|
|
31
|
-
# # => nil (validation passes)
|
|
32
|
-
# @example Include values within a range
|
|
33
|
-
# Inclusion.call(5, in: 1..10)
|
|
34
|
-
# # => nil (validation passes - 5 is within 1..10)
|
|
35
|
-
# @example Include with custom message
|
|
36
|
-
# Inclusion.call("test", in: ["admin", "user"], message: "must be one of: %{values}")
|
|
37
|
-
#
|
|
38
|
-
# @rbs (untyped value, Hash[Symbol, untyped] options) -> nil
|
|
11
|
+
# @param value [Object]
|
|
12
|
+
# @param options [Hash{Symbol => Object}]
|
|
13
|
+
# @option options [Range, Array, Set, Enumerable] :in allowed values
|
|
14
|
+
# @option options [Range, Array, Set, Enumerable] :within alias for `:in`
|
|
15
|
+
# @option options [String] :message global failure-message override
|
|
16
|
+
# @option options [String] :of_message override for enumerable failures
|
|
17
|
+
# @option options [String] :in_message, :within_message overrides for range failures
|
|
18
|
+
# @return [Validators::Failure, nil]
|
|
19
|
+
# @raise [ArgumentError] when neither `:in` nor `:within` is given
|
|
39
20
|
def call(value, options = EMPTY_HASH)
|
|
40
21
|
values = options[:in] || options[:within]
|
|
22
|
+
raise ArgumentError, "inclusion validator requires :in or :within option" if values.nil?
|
|
41
23
|
|
|
42
24
|
if values.is_a?(Range)
|
|
43
|
-
|
|
44
|
-
elsif
|
|
45
|
-
|
|
25
|
+
within_failure(values.begin, values.end, options) unless values.cover?(value)
|
|
26
|
+
elsif Array(values).none? { |v| v === value }
|
|
27
|
+
of_failure(values, options)
|
|
46
28
|
end
|
|
47
29
|
end
|
|
48
30
|
|
|
49
31
|
private
|
|
50
32
|
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
# @
|
|
54
|
-
# @
|
|
55
|
-
# @
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def raise_of_validation_error!(values, options)
|
|
59
|
-
values = values.map(&:inspect).join(", ") unless values.nil?
|
|
33
|
+
# @param values [Enumerable] collection rendered into the failure message
|
|
34
|
+
# @param options [Hash{Symbol => Object}]
|
|
35
|
+
# @option options [String] :of_message
|
|
36
|
+
# @option options [String] :message
|
|
37
|
+
# @return [Validators::Failure]
|
|
38
|
+
def of_failure(values, options)
|
|
39
|
+
values = values.map(&:inspect).join(", ")
|
|
60
40
|
message = options[:of_message] || options[:message]
|
|
61
41
|
message %= { values: } unless message.nil?
|
|
62
42
|
|
|
63
|
-
|
|
43
|
+
Failure.new(message || I18nProxy.t("cmdx.validators.inclusion.of", values:))
|
|
64
44
|
end
|
|
65
45
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
# @param
|
|
69
|
-
# @
|
|
70
|
-
# @
|
|
71
|
-
# @option options [
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
def raise_within_validation_error!(min, max, options)
|
|
46
|
+
# @param min [Object] range/inclusion lower bound
|
|
47
|
+
# @param max [Object] range/inclusion upper bound
|
|
48
|
+
# @param options [Hash{Symbol => Object}]
|
|
49
|
+
# @option options [String] :in_message
|
|
50
|
+
# @option options [String] :within_message
|
|
51
|
+
# @option options [String] :message
|
|
52
|
+
# @return [Validators::Failure]
|
|
53
|
+
def within_failure(min, max, options)
|
|
75
54
|
message = options[:in_message] || options[:within_message] || options[:message]
|
|
76
55
|
message %= { min:, max: } unless message.nil?
|
|
77
56
|
|
|
78
|
-
|
|
57
|
+
Failure.new(message || I18nProxy.t("cmdx.validators.inclusion.within", min:, max:))
|
|
79
58
|
end
|
|
80
59
|
|
|
81
60
|
end
|