cmdx 1.18.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/CHANGELOG.md +73 -9
- data/README.md +1 -1
- data/lib/cmdx/attribute.rb +88 -20
- data/lib/cmdx/attribute_registry.rb +79 -8
- data/lib/cmdx/attribute_value.rb +8 -3
- data/lib/cmdx/callback_registry.rb +60 -26
- data/lib/cmdx/chain.rb +47 -4
- data/lib/cmdx/coercion_registry.rb +42 -20
- data/lib/cmdx/coercions/array.rb +8 -3
- data/lib/cmdx/coercions/big_decimal.rb +1 -1
- data/lib/cmdx/coercions/boolean.rb +6 -2
- data/lib/cmdx/coercions/complex.rb +1 -1
- data/lib/cmdx/coercions/date.rb +2 -7
- data/lib/cmdx/coercions/date_time.rb +2 -7
- data/lib/cmdx/coercions/float.rb +1 -1
- data/lib/cmdx/coercions/hash.rb +1 -1
- data/lib/cmdx/coercions/integer.rb +4 -5
- data/lib/cmdx/coercions/rational.rb +1 -1
- data/lib/cmdx/coercions/string.rb +1 -1
- data/lib/cmdx/coercions/symbol.rb +1 -1
- data/lib/cmdx/coercions/time.rb +1 -7
- data/lib/cmdx/configuration.rb +26 -0
- data/lib/cmdx/context.rb +9 -6
- data/lib/cmdx/deprecator.rb +27 -14
- data/lib/cmdx/errors.rb +3 -4
- data/lib/cmdx/exception.rb +7 -0
- data/lib/cmdx/executor.rb +77 -54
- data/lib/cmdx/identifier.rb +4 -6
- data/lib/cmdx/locale.rb +32 -9
- data/lib/cmdx/middleware_registry.rb +43 -23
- data/lib/cmdx/middlewares/correlate.rb +4 -2
- data/lib/cmdx/middlewares/timeout.rb +11 -10
- data/lib/cmdx/parallelizer.rb +100 -0
- data/lib/cmdx/pipeline.rb +42 -23
- data/lib/cmdx/railtie.rb +1 -1
- data/lib/cmdx/result.rb +27 -11
- data/lib/cmdx/retry.rb +166 -0
- data/lib/cmdx/settings.rb +222 -0
- data/lib/cmdx/task.rb +53 -61
- data/lib/cmdx/utils/format.rb +17 -1
- data/lib/cmdx/utils/normalize.rb +52 -0
- data/lib/cmdx/utils/wrap.rb +38 -0
- data/lib/cmdx/validator_registry.rb +45 -20
- data/lib/cmdx/validators/absence.rb +1 -1
- data/lib/cmdx/validators/exclusion.rb +2 -2
- data/lib/cmdx/validators/format.rb +1 -1
- data/lib/cmdx/validators/inclusion.rb +2 -2
- data/lib/cmdx/validators/length.rb +1 -1
- data/lib/cmdx/validators/numeric.rb +1 -1
- data/lib/cmdx/validators/presence.rb +1 -1
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx.rb +12 -0
- data/lib/generators/cmdx/templates/install.rb +11 -0
- data/mkdocs.yml +5 -1
- metadata +6 -15
data/lib/cmdx/chain.rb
CHANGED
|
@@ -31,7 +31,7 @@ module CMDx
|
|
|
31
31
|
# @rbs @results: Array[Result]
|
|
32
32
|
attr_reader :results
|
|
33
33
|
|
|
34
|
-
def_delegators :results, :
|
|
34
|
+
def_delegators :results, :first, :last, :size
|
|
35
35
|
def_delegators :first, :state, :status, :outcome, :runtime
|
|
36
36
|
|
|
37
37
|
# Creates a new chain with a unique identifier and empty results collection.
|
|
@@ -40,6 +40,7 @@ module CMDx
|
|
|
40
40
|
#
|
|
41
41
|
# @rbs () -> void
|
|
42
42
|
def initialize(dry_run: false)
|
|
43
|
+
@mutex = Mutex.new
|
|
43
44
|
@id = Identifier.generate
|
|
44
45
|
@results = []
|
|
45
46
|
@dry_run = !!dry_run
|
|
@@ -107,7 +108,7 @@ module CMDx
|
|
|
107
108
|
raise TypeError, "must be a CMDx::Result" unless result.is_a?(Result)
|
|
108
109
|
|
|
109
110
|
self.current ||= new(dry_run:)
|
|
110
|
-
current.
|
|
111
|
+
current.push(result)
|
|
111
112
|
current
|
|
112
113
|
end
|
|
113
114
|
|
|
@@ -118,12 +119,40 @@ module CMDx
|
|
|
118
119
|
# @return [Hash] The thread or fiber storage
|
|
119
120
|
#
|
|
120
121
|
# @rbs () -> Hash
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
if Fiber.respond_to?(:storage)
|
|
123
|
+
def thread_or_fiber = Fiber.storage
|
|
124
|
+
else
|
|
125
|
+
def thread_or_fiber = Thread.current
|
|
123
126
|
end
|
|
124
127
|
|
|
125
128
|
end
|
|
126
129
|
|
|
130
|
+
# Thread-safe append of a result to the chain.
|
|
131
|
+
# Caches the result's index to avoid repeated O(n) lookups.
|
|
132
|
+
#
|
|
133
|
+
# @param result [Result] The result to append
|
|
134
|
+
#
|
|
135
|
+
# @return [Array<Result>] The updated results array
|
|
136
|
+
#
|
|
137
|
+
# @rbs (Result result) -> Array[Result]
|
|
138
|
+
def push(result)
|
|
139
|
+
@mutex.synchronize do
|
|
140
|
+
result.instance_variable_set(:@chain_index, @results.size)
|
|
141
|
+
@results << result
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Thread-safe lookup of a result's position in the chain.
|
|
146
|
+
#
|
|
147
|
+
# @param result [Result] The result to find
|
|
148
|
+
#
|
|
149
|
+
# @return [Integer, nil] The zero-based index or nil if not found
|
|
150
|
+
#
|
|
151
|
+
# @rbs (Result result) -> Integer?
|
|
152
|
+
def index(result)
|
|
153
|
+
@mutex.synchronize { @results.index(result) }
|
|
154
|
+
end
|
|
155
|
+
|
|
127
156
|
# Returns whether the chain is running in dry-run mode.
|
|
128
157
|
#
|
|
129
158
|
# @return [Boolean] Whether the chain is running in dry-run mode
|
|
@@ -136,6 +165,20 @@ module CMDx
|
|
|
136
165
|
!!@dry_run
|
|
137
166
|
end
|
|
138
167
|
|
|
168
|
+
# Freezes the chain and its internal results to prevent modifications.
|
|
169
|
+
#
|
|
170
|
+
# @return [Chain] the frozen chain
|
|
171
|
+
#
|
|
172
|
+
# @example
|
|
173
|
+
# chain.freeze
|
|
174
|
+
# chain.results << result # => raises FrozenError
|
|
175
|
+
#
|
|
176
|
+
# @rbs () -> self
|
|
177
|
+
def freeze
|
|
178
|
+
results.freeze
|
|
179
|
+
super
|
|
180
|
+
end
|
|
181
|
+
|
|
139
182
|
# Converts the chain to a hash representation.
|
|
140
183
|
#
|
|
141
184
|
# @option return [String] :id The chain identifier
|
|
@@ -5,19 +5,11 @@ module CMDx
|
|
|
5
5
|
#
|
|
6
6
|
# Provides a centralized way to register, deregister, and execute type coercions
|
|
7
7
|
# for various data types including arrays, numbers, dates, and other primitives.
|
|
8
|
+
#
|
|
9
|
+
# Supports copy-on-write semantics: a duped registry shares the parent's
|
|
10
|
+
# data until a write operation triggers materialization.
|
|
8
11
|
class CoercionRegistry
|
|
9
12
|
|
|
10
|
-
# Returns the internal registry mapping coercion types to handler classes.
|
|
11
|
-
#
|
|
12
|
-
# @return [Hash{Symbol => Class}] Hash of coercion type names to coercion classes
|
|
13
|
-
#
|
|
14
|
-
# @example
|
|
15
|
-
# registry.registry # => { integer: Coercions::Integer, boolean: Coercions::Boolean }
|
|
16
|
-
#
|
|
17
|
-
# @rbs @registry: Hash[Symbol, Class]
|
|
18
|
-
attr_reader :registry
|
|
19
|
-
alias to_h registry
|
|
20
|
-
|
|
21
13
|
# Initialize a new coercion registry.
|
|
22
14
|
#
|
|
23
15
|
# @param registry [Hash{Symbol => Class}, nil] optional initial registry hash
|
|
@@ -44,17 +36,30 @@ module CMDx
|
|
|
44
36
|
}
|
|
45
37
|
end
|
|
46
38
|
|
|
47
|
-
#
|
|
39
|
+
# Sets up copy-on-write state when duplicated via dup.
|
|
48
40
|
#
|
|
49
|
-
# @
|
|
41
|
+
# @param source [CoercionRegistry] The registry being duplicated
|
|
42
|
+
#
|
|
43
|
+
# @rbs (CoercionRegistry source) -> void
|
|
44
|
+
def initialize_dup(source)
|
|
45
|
+
@parent = source
|
|
46
|
+
@registry = nil
|
|
47
|
+
super
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns the internal registry mapping coercion types to handler classes.
|
|
51
|
+
# Delegates to the parent registry when not yet materialized.
|
|
52
|
+
#
|
|
53
|
+
# @return [Hash{Symbol => Class}] Hash of coercion type names to coercion classes
|
|
50
54
|
#
|
|
51
55
|
# @example
|
|
52
|
-
#
|
|
56
|
+
# registry.registry # => { integer: Coercions::Integer, boolean: Coercions::Boolean }
|
|
53
57
|
#
|
|
54
|
-
# @rbs () ->
|
|
55
|
-
def
|
|
56
|
-
|
|
58
|
+
# @rbs () -> Hash[Symbol, Class]
|
|
59
|
+
def registry
|
|
60
|
+
@registry || @parent.registry
|
|
57
61
|
end
|
|
62
|
+
alias to_h registry
|
|
58
63
|
|
|
59
64
|
# Register a new coercion handler for a type.
|
|
60
65
|
#
|
|
@@ -69,7 +74,9 @@ module CMDx
|
|
|
69
74
|
#
|
|
70
75
|
# @rbs ((Symbol | String) name, Class coercion) -> self
|
|
71
76
|
def register(name, coercion)
|
|
72
|
-
|
|
77
|
+
materialize!
|
|
78
|
+
|
|
79
|
+
@registry[name.to_sym] = coercion
|
|
73
80
|
self
|
|
74
81
|
end
|
|
75
82
|
|
|
@@ -85,7 +92,9 @@ module CMDx
|
|
|
85
92
|
#
|
|
86
93
|
# @rbs ((Symbol | String) name) -> self
|
|
87
94
|
def deregister(name)
|
|
88
|
-
|
|
95
|
+
materialize!
|
|
96
|
+
|
|
97
|
+
@registry.delete(name.to_sym)
|
|
89
98
|
self
|
|
90
99
|
end
|
|
91
100
|
|
|
@@ -106,11 +115,24 @@ module CMDx
|
|
|
106
115
|
# result = registry.coerce(:boolean, task, "true", strict: true)
|
|
107
116
|
#
|
|
108
117
|
# @rbs (Symbol type, untyped task, untyped value, ?Hash[Symbol, untyped] options) -> untyped
|
|
109
|
-
def coerce(type, task, value, options =
|
|
118
|
+
def coerce(type, task, value, options = EMPTY_HASH)
|
|
110
119
|
raise TypeError, "unknown coercion type #{type.inspect}" unless registry.key?(type)
|
|
111
120
|
|
|
112
121
|
Utils::Call.invoke(task, registry[type], value, options)
|
|
113
122
|
end
|
|
114
123
|
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
# Copies the parent's registry data into this instance,
|
|
127
|
+
# severing the copy-on-write link.
|
|
128
|
+
#
|
|
129
|
+
# @rbs () -> void
|
|
130
|
+
def materialize!
|
|
131
|
+
return if @registry
|
|
132
|
+
|
|
133
|
+
@registry = @parent.registry.dup
|
|
134
|
+
@parent = nil
|
|
135
|
+
end
|
|
136
|
+
|
|
115
137
|
end
|
|
116
138
|
end
|
data/lib/cmdx/coercions/array.rb
CHANGED
|
@@ -18,7 +18,7 @@ module CMDx
|
|
|
18
18
|
#
|
|
19
19
|
# @return [Array] The converted array value
|
|
20
20
|
#
|
|
21
|
-
# @raise [
|
|
21
|
+
# @raise [CoercionError] If the value cannot be converted to an array
|
|
22
22
|
#
|
|
23
23
|
# @example Convert a JSON-like string to an array
|
|
24
24
|
# Array.call("[1, 2, 3]") # => [1, 2, 3]
|
|
@@ -26,17 +26,22 @@ module CMDx
|
|
|
26
26
|
# Array.call("hello") # => ["hello"]
|
|
27
27
|
# Array.call(42) # => [42]
|
|
28
28
|
# Array.call(nil) # => []
|
|
29
|
+
# @example Handle invalid JSON-like strings
|
|
30
|
+
# Array.call("[not json") # => raises CoercionError
|
|
29
31
|
#
|
|
30
32
|
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Array[untyped]
|
|
31
|
-
def call(value, options =
|
|
33
|
+
def call(value, options = EMPTY_HASH)
|
|
32
34
|
if value.is_a?(::String) && (
|
|
33
35
|
value.start_with?("[") ||
|
|
34
36
|
value.strip == "null"
|
|
35
37
|
)
|
|
36
38
|
JSON.parse(value) || []
|
|
37
39
|
else
|
|
38
|
-
|
|
40
|
+
Utils::Wrap.array(value)
|
|
39
41
|
end
|
|
42
|
+
rescue JSON::ParserError
|
|
43
|
+
type = Locale.t("cmdx.types.array")
|
|
44
|
+
raise CoercionError, Locale.t("cmdx.coercions.into_an", type:)
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
end
|
|
@@ -31,7 +31,7 @@ module CMDx
|
|
|
31
31
|
# BigDecimal.call(3.14159) # => #<BigDecimal:7f8b8c0d8e0f '0.314159E1',9(18)>
|
|
32
32
|
#
|
|
33
33
|
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> BigDecimal
|
|
34
|
-
def call(value, options =
|
|
34
|
+
def call(value, options = EMPTY_HASH)
|
|
35
35
|
BigDecimal(value, options[:precision] || DEFAULT_PRECISION)
|
|
36
36
|
rescue ArgumentError, TypeError
|
|
37
37
|
type = Locale.t("cmdx.types.big_decimal")
|
|
@@ -34,14 +34,18 @@ module CMDx
|
|
|
34
34
|
# Boolean.call("false") # => false
|
|
35
35
|
# Boolean.call("no") # => false
|
|
36
36
|
# Boolean.call("0") # => false
|
|
37
|
+
# Boolean.call(nil) # => false
|
|
38
|
+
# Boolean.call("") # => false
|
|
37
39
|
# @example Handle case-insensitive input
|
|
38
40
|
# Boolean.call("TRUE") # => true
|
|
39
41
|
# Boolean.call("False") # => false
|
|
42
|
+
# @example Handle edge cases
|
|
43
|
+
# Boolean.call("abc") # => raises CoercionError
|
|
40
44
|
#
|
|
41
45
|
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> bool
|
|
42
|
-
def call(value, options =
|
|
46
|
+
def call(value, options = EMPTY_HASH)
|
|
43
47
|
case value.to_s
|
|
44
|
-
when FALSEY then false
|
|
48
|
+
when FALSEY, EMPTY_STRING then false
|
|
45
49
|
when TRUTHY then true
|
|
46
50
|
else
|
|
47
51
|
type = Locale.t("cmdx.types.boolean")
|
|
@@ -29,7 +29,7 @@ module CMDx
|
|
|
29
29
|
# Complex.call(Complex(1, 2)) # => (1+2i)
|
|
30
30
|
#
|
|
31
31
|
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Complex
|
|
32
|
-
def call(value, options =
|
|
32
|
+
def call(value, options = EMPTY_HASH)
|
|
33
33
|
Complex(value)
|
|
34
34
|
rescue ArgumentError, TypeError
|
|
35
35
|
type = Locale.t("cmdx.types.complex")
|
data/lib/cmdx/coercions/date.rb
CHANGED
|
@@ -11,11 +11,6 @@ module CMDx
|
|
|
11
11
|
|
|
12
12
|
extend self
|
|
13
13
|
|
|
14
|
-
# Types that are already date-like and don't need conversion
|
|
15
|
-
#
|
|
16
|
-
# @rbs ANALOG_TYPES: Array[String]
|
|
17
|
-
ANALOG_TYPES = %w[Date DateTime Time].freeze
|
|
18
|
-
|
|
19
14
|
# Converts a value to a Date object
|
|
20
15
|
#
|
|
21
16
|
# @param value [Object] The value to convert to a Date
|
|
@@ -37,8 +32,8 @@ module CMDx
|
|
|
37
32
|
# Date.call(DateTime.new(2023, 12, 25)) # => #<Date: 2023-12-25>
|
|
38
33
|
#
|
|
39
34
|
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Date
|
|
40
|
-
def call(value, options =
|
|
41
|
-
return value if
|
|
35
|
+
def call(value, options = EMPTY_HASH)
|
|
36
|
+
return value.to_date if value.respond_to?(:to_date)
|
|
42
37
|
return ::Date.strptime(value, options[:strptime]) if options[:strptime]
|
|
43
38
|
|
|
44
39
|
::Date.parse(value)
|
|
@@ -11,11 +11,6 @@ module CMDx
|
|
|
11
11
|
|
|
12
12
|
extend self
|
|
13
13
|
|
|
14
|
-
# Types that are already date-time-like and don't need conversion
|
|
15
|
-
#
|
|
16
|
-
# @rbs ANALOG_TYPES: Array[String]
|
|
17
|
-
ANALOG_TYPES = %w[Date DateTime Time].freeze
|
|
18
|
-
|
|
19
14
|
# Converts a value to a DateTime
|
|
20
15
|
#
|
|
21
16
|
# @param value [Object] The value to convert to DateTime
|
|
@@ -37,8 +32,8 @@ module CMDx
|
|
|
37
32
|
# DateTime.call(Time.new(2023, 12, 25)) # => #<DateTime: 2023-12-25T00:00:00+00:00>
|
|
38
33
|
#
|
|
39
34
|
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> DateTime
|
|
40
|
-
def call(value, options =
|
|
41
|
-
return value if
|
|
35
|
+
def call(value, options = EMPTY_HASH)
|
|
36
|
+
return value.to_datetime if value.respond_to?(:to_datetime)
|
|
42
37
|
return ::DateTime.strptime(value, options[:strptime]) if options[:strptime]
|
|
43
38
|
|
|
44
39
|
::DateTime.parse(value)
|
data/lib/cmdx/coercions/float.rb
CHANGED
|
@@ -32,7 +32,7 @@ module CMDx
|
|
|
32
32
|
# Float.call(Complex(5.0, 0)) # => 5.0
|
|
33
33
|
#
|
|
34
34
|
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Float
|
|
35
|
-
def call(value, options =
|
|
35
|
+
def call(value, options = EMPTY_HASH)
|
|
36
36
|
Float(value)
|
|
37
37
|
rescue ArgumentError, RangeError, TypeError
|
|
38
38
|
type = Locale.t("cmdx.types.float")
|
data/lib/cmdx/coercions/hash.rb
CHANGED
|
@@ -33,7 +33,7 @@ module CMDx
|
|
|
33
33
|
# Hash.call('{"key": "value"}') # => {"key" => "value"}
|
|
34
34
|
#
|
|
35
35
|
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Hash[untyped, untyped]
|
|
36
|
-
def call(value, options =
|
|
36
|
+
def call(value, options = EMPTY_HASH)
|
|
37
37
|
if value.nil?
|
|
38
38
|
{}
|
|
39
39
|
elsif value.is_a?(::Hash)
|
|
@@ -30,13 +30,12 @@ module CMDx
|
|
|
30
30
|
# Integer.call(3.14) # => 3
|
|
31
31
|
# Integer.call(0.0) # => 0
|
|
32
32
|
# @example Handle edge cases
|
|
33
|
-
# Integer.call("") # =>
|
|
34
|
-
# Integer.call(nil) # =>
|
|
35
|
-
# Integer.call(
|
|
36
|
-
# Integer.call(true) # => 1
|
|
33
|
+
# Integer.call("") # => raises CoercionError
|
|
34
|
+
# Integer.call(nil) # => raises CoercionError
|
|
35
|
+
# Integer.call("abc") # => raises CoercionError
|
|
37
36
|
#
|
|
38
37
|
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Integer
|
|
39
|
-
def call(value, options =
|
|
38
|
+
def call(value, options = EMPTY_HASH)
|
|
40
39
|
Integer(value)
|
|
41
40
|
rescue ArgumentError, FloatDomainError, RangeError, TypeError
|
|
42
41
|
type = Locale.t("cmdx.types.integer")
|
|
@@ -35,7 +35,7 @@ module CMDx
|
|
|
35
35
|
# Rational.call(0) # => (0/1)
|
|
36
36
|
#
|
|
37
37
|
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Rational
|
|
38
|
-
def call(value, options =
|
|
38
|
+
def call(value, options = EMPTY_HASH)
|
|
39
39
|
Rational(value)
|
|
40
40
|
rescue ArgumentError, FloatDomainError, RangeError, TypeError, ZeroDivisionError
|
|
41
41
|
type = Locale.t("cmdx.types.rational")
|
|
@@ -28,7 +28,7 @@ module CMDx
|
|
|
28
28
|
# Symbol.call(:existing) # => :existing
|
|
29
29
|
#
|
|
30
30
|
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Symbol
|
|
31
|
-
def call(value, options =
|
|
31
|
+
def call(value, options = EMPTY_HASH)
|
|
32
32
|
value.to_sym
|
|
33
33
|
rescue NoMethodError
|
|
34
34
|
type = Locale.t("cmdx.types.symbol")
|
data/lib/cmdx/coercions/time.rb
CHANGED
|
@@ -11,11 +11,6 @@ module CMDx
|
|
|
11
11
|
|
|
12
12
|
extend self
|
|
13
13
|
|
|
14
|
-
# Types that are already time-like and don't need conversion
|
|
15
|
-
#
|
|
16
|
-
# @rbs ANALOG_TYPES: Array[String]
|
|
17
|
-
ANALOG_TYPES = %w[DateTime Time].freeze
|
|
18
|
-
|
|
19
14
|
# Converts a value to a Time object
|
|
20
15
|
#
|
|
21
16
|
# @param value [Object] The value to convert to a Time object
|
|
@@ -39,8 +34,7 @@ module CMDx
|
|
|
39
34
|
# Time.call("12-25-2023", strptime: "%m-%d-%Y") # => Time object
|
|
40
35
|
#
|
|
41
36
|
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Time
|
|
42
|
-
def call(value, options =
|
|
43
|
-
return value if ANALOG_TYPES.include?(value.class.name)
|
|
37
|
+
def call(value, options = EMPTY_HASH)
|
|
44
38
|
return value.to_time if value.respond_to?(:to_time)
|
|
45
39
|
return ::Time.strptime(value, options[:strptime]) if options[:strptime]
|
|
46
40
|
|
data/lib/cmdx/configuration.rb
CHANGED
|
@@ -123,6 +123,28 @@ module CMDx
|
|
|
123
123
|
# @rbs @rollback_on: Array[String]
|
|
124
124
|
attr_accessor :rollback_on
|
|
125
125
|
|
|
126
|
+
# Returns whether to freeze task results after execution.
|
|
127
|
+
# Set to false in test environments to allow stubbing on result objects.
|
|
128
|
+
#
|
|
129
|
+
# @return [Boolean] true if results should be frozen (default: true)
|
|
130
|
+
#
|
|
131
|
+
# @example
|
|
132
|
+
# config.freeze_results = false
|
|
133
|
+
#
|
|
134
|
+
# @rbs @freeze_results: bool
|
|
135
|
+
attr_accessor :freeze_results
|
|
136
|
+
|
|
137
|
+
# Returns the default locale used for built-in translation lookups.
|
|
138
|
+
# Must match the basename of a YAML file in lib/locales/ (e.g. "en", "es", "ja").
|
|
139
|
+
#
|
|
140
|
+
# @return [String] The locale identifier (default: "en")
|
|
141
|
+
#
|
|
142
|
+
# @example
|
|
143
|
+
# config.default_locale = "es"
|
|
144
|
+
#
|
|
145
|
+
# @rbs @locale: String
|
|
146
|
+
attr_accessor :default_locale
|
|
147
|
+
|
|
126
148
|
# Initializes a new Configuration instance with default values.
|
|
127
149
|
#
|
|
128
150
|
# Creates new registry instances for middlewares, callbacks, coercions, and
|
|
@@ -145,6 +167,8 @@ module CMDx
|
|
|
145
167
|
@task_breakpoints = DEFAULT_BREAKPOINTS
|
|
146
168
|
@workflow_breakpoints = DEFAULT_BREAKPOINTS
|
|
147
169
|
@rollback_on = DEFAULT_ROLLPOINTS
|
|
170
|
+
@freeze_results = true
|
|
171
|
+
@default_locale = "en"
|
|
148
172
|
|
|
149
173
|
@backtrace = false
|
|
150
174
|
@backtrace_cleaner = nil
|
|
@@ -177,6 +201,8 @@ module CMDx
|
|
|
177
201
|
task_breakpoints: @task_breakpoints,
|
|
178
202
|
workflow_breakpoints: @workflow_breakpoints,
|
|
179
203
|
rollback_on: @rollback_on,
|
|
204
|
+
freeze_results: @freeze_results,
|
|
205
|
+
default_locale: @default_locale,
|
|
180
206
|
backtrace: @backtrace,
|
|
181
207
|
backtrace_cleaner: @backtrace_cleaner,
|
|
182
208
|
exception_handler: @exception_handler,
|
data/lib/cmdx/context.rb
CHANGED
|
@@ -160,8 +160,8 @@ module CMDx
|
|
|
160
160
|
# context.to_h # => {name: "John", age: 30, city: "NYC"}
|
|
161
161
|
#
|
|
162
162
|
# @rbs (?untyped args) -> self
|
|
163
|
-
def merge!(args =
|
|
164
|
-
args.to_h.
|
|
163
|
+
def merge!(args = EMPTY_HASH)
|
|
164
|
+
table.merge!(args.to_h.transform_keys(&:to_sym))
|
|
165
165
|
self
|
|
166
166
|
end
|
|
167
167
|
alias merge merge!
|
|
@@ -280,13 +280,15 @@ module CMDx
|
|
|
280
280
|
#
|
|
281
281
|
# @rbs (Symbol method_name, *untyped args, **untyped _kwargs) ?{ () -> untyped } -> untyped
|
|
282
282
|
def method_missing(method_name, *args, **_kwargs, &)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
283
|
+
if method_name.end_with?("=")
|
|
284
|
+
store(method_name.name.chop, args.first)
|
|
285
|
+
else
|
|
286
|
+
table[method_name]
|
|
286
287
|
end
|
|
287
288
|
end
|
|
288
289
|
|
|
289
290
|
# Checks if the object responds to a given method.
|
|
291
|
+
# Supports both getter access for existing keys and setter methods.
|
|
290
292
|
#
|
|
291
293
|
# @param method_name [Symbol] the method name to check
|
|
292
294
|
# @param include_private [Boolean] whether to include private methods
|
|
@@ -296,11 +298,12 @@ module CMDx
|
|
|
296
298
|
# @example
|
|
297
299
|
# context = Context.new(name: "John")
|
|
298
300
|
# context.respond_to?(:name) # => true
|
|
301
|
+
# context.respond_to?(:name=) # => true
|
|
299
302
|
# context.respond_to?(:age) # => false
|
|
300
303
|
#
|
|
301
304
|
# @rbs (Symbol method_name, ?bool include_private) -> bool
|
|
302
305
|
def respond_to_missing?(method_name, include_private = false)
|
|
303
|
-
key?(method_name) || super
|
|
306
|
+
key?(method_name) || method_name.end_with?("=") || super
|
|
304
307
|
end
|
|
305
308
|
|
|
306
309
|
end
|
data/lib/cmdx/deprecator.rb
CHANGED
|
@@ -10,11 +10,23 @@ module CMDx
|
|
|
10
10
|
|
|
11
11
|
extend self
|
|
12
12
|
|
|
13
|
+
# @rbs RAISE_REGEXP: Regexp
|
|
14
|
+
RAISE_REGEXP = /\Araise\z/
|
|
15
|
+
private_constant :RAISE_REGEXP
|
|
16
|
+
|
|
17
|
+
# @rbs LOG_REGEXP: Regexp
|
|
18
|
+
LOG_REGEXP = /\Alog\z/
|
|
19
|
+
private_constant :LOG_REGEXP
|
|
20
|
+
|
|
21
|
+
# @rbs WARN_REGEXP: Regexp
|
|
22
|
+
WARN_REGEXP = /\Awarn\z/
|
|
23
|
+
private_constant :WARN_REGEXP
|
|
24
|
+
|
|
13
25
|
# @rbs EVAL: Proc
|
|
14
26
|
EVAL = proc do |target, callable|
|
|
15
27
|
case callable
|
|
16
|
-
when /raise|log|warn/ then callable
|
|
17
28
|
when NilClass, FalseClass, TrueClass then !!callable
|
|
29
|
+
when RAISE_REGEXP, LOG_REGEXP, WARN_REGEXP then callable
|
|
18
30
|
when Symbol then target.send(callable)
|
|
19
31
|
when Proc then target.instance_eval(&callable)
|
|
20
32
|
else
|
|
@@ -28,14 +40,14 @@ module CMDx
|
|
|
28
40
|
# Restricts task usage based on deprecation settings.
|
|
29
41
|
#
|
|
30
42
|
# @param task [Object] The task object to check for deprecation
|
|
31
|
-
# @option task.class.settings
|
|
43
|
+
# @option task.class.settings.deprecate [Symbol, Proc, String, Boolean]
|
|
32
44
|
# The deprecation configuration for the task
|
|
33
|
-
# @option task.class.settings
|
|
34
|
-
# @option task.class.settings
|
|
35
|
-
# @option task.class.settings
|
|
36
|
-
# @option task.class.settings
|
|
37
|
-
# @option task.class.settings
|
|
38
|
-
# @option task.class.settings
|
|
45
|
+
# @option task.class.settings.deprecate :raise Raises DeprecationError
|
|
46
|
+
# @option task.class.settings.deprecate :log Logs deprecation warning
|
|
47
|
+
# @option task.class.settings.deprecate :warn Outputs warning to stderr
|
|
48
|
+
# @option task.class.settings.deprecate true Raises DeprecationError
|
|
49
|
+
# @option task.class.settings.deprecate false No action taken
|
|
50
|
+
# @option task.class.settings.deprecate nil No action taken
|
|
39
51
|
#
|
|
40
52
|
# @raise [DeprecationError] When deprecation type is :raise or true
|
|
41
53
|
# @raise [RuntimeError] When deprecation type is unknown
|
|
@@ -49,13 +61,14 @@ module CMDx
|
|
|
49
61
|
#
|
|
50
62
|
# @rbs (Task task) -> void
|
|
51
63
|
def restrict(task)
|
|
52
|
-
|
|
64
|
+
setting = task.class.settings.deprecate
|
|
65
|
+
return unless setting
|
|
53
66
|
|
|
54
|
-
case type
|
|
55
|
-
when NilClass, FalseClass # Do nothing
|
|
56
|
-
when TrueClass,
|
|
57
|
-
when
|
|
58
|
-
when
|
|
67
|
+
case type = EVAL.call(task, setting)
|
|
68
|
+
when NilClass, FalseClass then nil # Do nothing
|
|
69
|
+
when TrueClass, RAISE_REGEXP then raise DeprecationError, "#{task.class.name} usage prohibited"
|
|
70
|
+
when LOG_REGEXP then task.logger.warn { "DEPRECATED: migrate to a replacement or discontinue use" }
|
|
71
|
+
when WARN_REGEXP then warn("[#{task.class.name}] DEPRECATED: migrate to a replacement or discontinue use", category: :deprecated)
|
|
59
72
|
else raise "unknown deprecation type #{type.inspect}"
|
|
60
73
|
end
|
|
61
74
|
end
|
data/lib/cmdx/errors.rb
CHANGED
|
@@ -18,7 +18,7 @@ module CMDx
|
|
|
18
18
|
# @rbs @messages: Hash[Symbol, Set[String]]
|
|
19
19
|
attr_reader :messages
|
|
20
20
|
|
|
21
|
-
def_delegators :messages, :empty
|
|
21
|
+
def_delegators :messages, :any?, :clear, :empty?, :size
|
|
22
22
|
|
|
23
23
|
# Initialize a new error collection.
|
|
24
24
|
#
|
|
@@ -57,9 +57,8 @@ module CMDx
|
|
|
57
57
|
#
|
|
58
58
|
# @rbs (Symbol attribute) -> bool
|
|
59
59
|
def for?(attribute)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
!messages[attribute].empty?
|
|
60
|
+
set = messages[attribute]
|
|
61
|
+
!set.nil? && !set.empty?
|
|
63
62
|
end
|
|
64
63
|
|
|
65
64
|
# Convert errors to a hash format with arrays of full messages.
|
data/lib/cmdx/exception.rb
CHANGED
|
@@ -29,6 +29,13 @@ module CMDx
|
|
|
29
29
|
# of required functionality.
|
|
30
30
|
UndefinedMethodError = Class.new(Error)
|
|
31
31
|
|
|
32
|
+
# Error raised when task execution exceeds the configured timeout limit.
|
|
33
|
+
#
|
|
34
|
+
# This error occurs when a task takes longer to execute than the specified
|
|
35
|
+
# time limit. Timeout errors are raised by Ruby's Timeout module and are
|
|
36
|
+
# caught by the middleware to properly fail the task with timeout information.
|
|
37
|
+
TimeoutError = Class.new(Interrupt)
|
|
38
|
+
|
|
32
39
|
# Raised when attribute validation fails during task execution.
|
|
33
40
|
#
|
|
34
41
|
# This error occurs when a attribute value doesn't meet the validation criteria
|