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
|
@@ -1,45 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# values that can be converted to DateTime using Ruby's DateTime.parse method
|
|
9
|
-
# or custom strptime formats.
|
|
4
|
+
class Coercions
|
|
5
|
+
# Coerces to `DateTime`. Pass `strptime:` to parse via a specific format;
|
|
6
|
+
# otherwise `DateTime.parse` is used for strings, and `#to_datetime` for
|
|
7
|
+
# any other responding object.
|
|
10
8
|
module DateTime
|
|
11
9
|
|
|
12
10
|
extend self
|
|
13
11
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# @
|
|
17
|
-
# @
|
|
18
|
-
# @option options [String] :strptime Custom date format string for parsing
|
|
19
|
-
#
|
|
20
|
-
# @return [DateTime] The converted DateTime value
|
|
21
|
-
#
|
|
22
|
-
# @raise [CoercionError] If the value cannot be converted to DateTime
|
|
23
|
-
#
|
|
24
|
-
# @example Convert date strings to DateTime
|
|
25
|
-
# DateTime.call("2023-12-25") # => #<DateTime: 2023-12-25T00:00:00+00:00>
|
|
26
|
-
# DateTime.call("Dec 25, 2023") # => #<DateTime: 2023-12-25T00:00:00+00:00>
|
|
27
|
-
# @example Convert with custom strptime format
|
|
28
|
-
# DateTime.call("25/12/2023", strptime: "%d/%m/%Y")
|
|
29
|
-
# # => #<DateTime: 2023-12-25T00:00:00+00:00>
|
|
30
|
-
# @example Convert existing date objects
|
|
31
|
-
# DateTime.call(Date.new(2023, 12, 25)) # => #<DateTime: 2023-12-25T00:00:00+00:00>
|
|
32
|
-
# DateTime.call(Time.new(2023, 12, 25)) # => #<DateTime: 2023-12-25T00:00:00+00:00>
|
|
33
|
-
#
|
|
34
|
-
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> DateTime
|
|
12
|
+
# @param value [Object]
|
|
13
|
+
# @param options [Hash{Symbol => Object}]
|
|
14
|
+
# @option options [String] :strptime format string for `DateTime.strptime`
|
|
15
|
+
# @return [DateTime, Coercions::Failure]
|
|
35
16
|
def call(value, options = EMPTY_HASH)
|
|
36
|
-
|
|
37
|
-
|
|
17
|
+
if value.is_a?(::DateTime)
|
|
18
|
+
value
|
|
19
|
+
elsif value.is_a?(::String)
|
|
20
|
+
if (strptime = options[:strptime])
|
|
21
|
+
::DateTime.strptime(value, strptime)
|
|
22
|
+
else
|
|
23
|
+
::DateTime.parse(value)
|
|
24
|
+
end
|
|
25
|
+
elsif value.respond_to?(:to_datetime)
|
|
26
|
+
value.to_datetime
|
|
27
|
+
else
|
|
28
|
+
coercion_failure
|
|
29
|
+
end
|
|
30
|
+
rescue ArgumentError, TypeError, ::Date::Error
|
|
31
|
+
coercion_failure
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
raise CoercionError, Locale.t("cmdx.coercions.into_a", type:)
|
|
36
|
+
def coercion_failure
|
|
37
|
+
type = I18nProxy.t("cmdx.types.date_time")
|
|
38
|
+
Failure.new(I18nProxy.t("cmdx.coercions.into_a", type:))
|
|
43
39
|
end
|
|
44
40
|
|
|
45
41
|
end
|
data/lib/cmdx/coercions/float.rb
CHANGED
|
@@ -1,42 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# Handles conversion from numeric strings, integers, and other numeric types
|
|
8
|
-
# that can be converted to floats using Ruby's Float() method.
|
|
4
|
+
class Coercions
|
|
5
|
+
# Coerces to Float via `Kernel#Float` (strict parsing; no silent zero).
|
|
9
6
|
module Float
|
|
10
7
|
|
|
11
8
|
extend self
|
|
12
9
|
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# @
|
|
16
|
-
# @
|
|
17
|
-
# @option options [Object] :unused Currently no options are used
|
|
18
|
-
#
|
|
19
|
-
# @return [Float] The converted float value
|
|
20
|
-
#
|
|
21
|
-
# @raise [CoercionError] If the value cannot be converted to a float
|
|
22
|
-
#
|
|
23
|
-
# @example Convert numeric strings to float
|
|
24
|
-
# Float.call("123") # => 123.0
|
|
25
|
-
# Float.call("123.456") # => 123.456
|
|
26
|
-
# Float.call("-42.5") # => -42.5
|
|
27
|
-
# Float.call("1.23e4") # => 12300.0
|
|
28
|
-
# @example Convert numeric types to float
|
|
29
|
-
# Float.call(42) # => 42.0
|
|
30
|
-
# Float.call(BigDecimal("123.456")) # => 123.456
|
|
31
|
-
# Float.call(Rational(3, 4)) # => 0.75
|
|
32
|
-
# Float.call(Complex(5.0, 0)) # => 5.0
|
|
33
|
-
#
|
|
34
|
-
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Float
|
|
10
|
+
# @param value [Object]
|
|
11
|
+
# @param options [Hash{Symbol => Object}]
|
|
12
|
+
# @option options [Object] reserved for future per-coercion configuration (currently ignored)
|
|
13
|
+
# @return [Float, Coercions::Failure]
|
|
35
14
|
def call(value, options = EMPTY_HASH)
|
|
36
15
|
Float(value)
|
|
37
16
|
rescue ArgumentError, RangeError, TypeError
|
|
38
|
-
type =
|
|
39
|
-
|
|
17
|
+
type = I18nProxy.t("cmdx.types.float")
|
|
18
|
+
Failure.new(I18nProxy.t("cmdx.coercions.into_a", type:))
|
|
40
19
|
end
|
|
41
20
|
|
|
42
21
|
end
|
data/lib/cmdx/coercions/hash.rb
CHANGED
|
@@ -1,67 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
# Coerces
|
|
6
|
-
#
|
|
7
|
-
# Supports conversion from:
|
|
8
|
-
# - Nil values (converted to empty Hash)
|
|
9
|
-
# - Hash objects (returned as-is)
|
|
10
|
-
# - Array objects (converted using Hash[*array])
|
|
11
|
-
# - JSON strings starting with "{" (parsed into Hash)
|
|
12
|
-
# - JSON strings that are "null" (parsed into empty Hash)
|
|
13
|
-
# - Other types raise CoercionError
|
|
4
|
+
class Coercions
|
|
5
|
+
# Coerces to Hash. `nil` becomes `{}`; strings are JSON-decoded (and
|
|
6
|
+
# must decode to a Hash); `#to_hash`/`#to_h` are used as fallbacks.
|
|
14
7
|
module Hash
|
|
15
8
|
|
|
16
9
|
extend self
|
|
17
10
|
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# @
|
|
21
|
-
# @
|
|
22
|
-
# @option options [Symbol] :strict Whether to enforce strict conversion
|
|
23
|
-
#
|
|
24
|
-
# @return [Hash] The coerced hash value
|
|
25
|
-
#
|
|
26
|
-
# @raise [CoercionError] When the value cannot be coerced to a Hash
|
|
27
|
-
#
|
|
28
|
-
# @example Coerce from existing Hash
|
|
29
|
-
# Hash.call({a: 1, b: 2}) # => {a: 1, b: 2}
|
|
30
|
-
# @example Coerce from Array
|
|
31
|
-
# Hash.call([:a, 1, :b, 2]) # => {a: 1, b: 2}
|
|
32
|
-
# @example Coerce from JSON string
|
|
33
|
-
# Hash.call('{"key": "value"}') # => {"key" => "value"}
|
|
34
|
-
#
|
|
35
|
-
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Hash[untyped, untyped]
|
|
11
|
+
# @param value [Object]
|
|
12
|
+
# @param options [Hash{Symbol => Object}]
|
|
13
|
+
# @option options [Object] reserved for future per-coercion configuration (currently ignored)
|
|
14
|
+
# @return [Hash, Coercions::Failure]
|
|
36
15
|
def call(value, options = EMPTY_HASH)
|
|
37
16
|
if value.nil?
|
|
38
17
|
{}
|
|
39
18
|
elsif value.is_a?(::Hash)
|
|
40
19
|
value
|
|
41
|
-
elsif value.is_a?(::
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
value.
|
|
46
|
-
)
|
|
47
|
-
JSON.parse(value) || {}
|
|
20
|
+
elsif value.is_a?(::String)
|
|
21
|
+
result = JSON.parse(value)
|
|
22
|
+
result.is_a?(::Hash) ? result : coercion_failure
|
|
23
|
+
elsif value.respond_to?(:to_hash)
|
|
24
|
+
value.to_hash
|
|
48
25
|
elsif value.respond_to?(:to_h)
|
|
49
26
|
value.to_h
|
|
50
27
|
else
|
|
51
|
-
|
|
28
|
+
coercion_failure
|
|
52
29
|
end
|
|
53
30
|
rescue ArgumentError, TypeError, JSON::ParserError
|
|
54
|
-
|
|
31
|
+
coercion_failure
|
|
55
32
|
end
|
|
56
33
|
|
|
57
34
|
private
|
|
58
35
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def raise_coercion_error!
|
|
63
|
-
type = Locale.t("cmdx.types.hash")
|
|
64
|
-
raise CoercionError, Locale.t("cmdx.coercions.into_a", type:)
|
|
36
|
+
def coercion_failure
|
|
37
|
+
type = I18nProxy.t("cmdx.types.hash")
|
|
38
|
+
Failure.new(I18nProxy.t("cmdx.coercions.into_a", type:))
|
|
65
39
|
end
|
|
66
40
|
|
|
67
41
|
end
|
|
@@ -1,45 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# Handles conversion from strings, numbers, and other values to integers
|
|
8
|
-
# using Ruby's Integer() method. Raises CoercionError for values that
|
|
9
|
-
# cannot be converted to integers.
|
|
4
|
+
class Coercions
|
|
5
|
+
# Coerces to Integer via `Kernel#Integer` (strict; rejects floats-as-strings).
|
|
10
6
|
module Integer
|
|
11
7
|
|
|
12
8
|
extend self
|
|
13
9
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# @
|
|
17
|
-
# @
|
|
18
|
-
# @option options [Object] :unused Currently no options are used
|
|
19
|
-
#
|
|
20
|
-
# @return [Integer] The converted integer value
|
|
21
|
-
#
|
|
22
|
-
# @raise [CoercionError] If the value cannot be converted to an integer
|
|
23
|
-
#
|
|
24
|
-
# @example Convert numeric strings to integers
|
|
25
|
-
# Integer.call("42") # => 42
|
|
26
|
-
# Integer.call("-123") # => -123
|
|
27
|
-
# Integer.call("0") # => 0
|
|
28
|
-
# @example Convert numeric types to integers
|
|
29
|
-
# Integer.call(42.0) # => 42
|
|
30
|
-
# Integer.call(3.14) # => 3
|
|
31
|
-
# Integer.call(0.0) # => 0
|
|
32
|
-
# @example Handle edge cases
|
|
33
|
-
# Integer.call("") # => raises CoercionError
|
|
34
|
-
# Integer.call(nil) # => raises CoercionError
|
|
35
|
-
# Integer.call("abc") # => raises CoercionError
|
|
36
|
-
#
|
|
37
|
-
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Integer
|
|
10
|
+
# @param value [Object]
|
|
11
|
+
# @param options [Hash{Symbol => Object}]
|
|
12
|
+
# @option options [Object] reserved for future per-coercion configuration (currently ignored)
|
|
13
|
+
# @return [Integer, Coercions::Failure]
|
|
38
14
|
def call(value, options = EMPTY_HASH)
|
|
39
15
|
Integer(value)
|
|
40
16
|
rescue ArgumentError, FloatDomainError, RangeError, TypeError
|
|
41
|
-
type =
|
|
42
|
-
|
|
17
|
+
type = I18nProxy.t("cmdx.types.integer")
|
|
18
|
+
Failure.new(I18nProxy.t("cmdx.coercions.into_an", type:))
|
|
43
19
|
end
|
|
44
20
|
|
|
45
21
|
end
|
|
@@ -1,45 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# Handles conversion from strings, numbers, and other values to rational
|
|
8
|
-
# numbers using Ruby's Rational() method. Raises CoercionError for values
|
|
9
|
-
# that cannot be converted to rational numbers.
|
|
4
|
+
class Coercions
|
|
5
|
+
# Coerces to `Rational`. Supply `denominator:` to build a rational from
|
|
6
|
+
# a numerator and a custom denominator.
|
|
10
7
|
module Rational
|
|
11
8
|
|
|
12
9
|
extend self
|
|
13
10
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# @
|
|
17
|
-
# @
|
|
18
|
-
# @option options [Object] :unused Currently no options are used
|
|
19
|
-
#
|
|
20
|
-
# @return [Rational] The converted rational number
|
|
21
|
-
#
|
|
22
|
-
# @raise [CoercionError] If the value cannot be converted to a rational number
|
|
23
|
-
#
|
|
24
|
-
# @example Convert numeric strings to rational numbers
|
|
25
|
-
# Rational.call("3/4") # => (3/4)
|
|
26
|
-
# Rational.call("2.5") # => (5/2)
|
|
27
|
-
# Rational.call("0") # => (0/1)
|
|
28
|
-
# @example Convert numeric types to rational numbers
|
|
29
|
-
# Rational.call(3.14) # => (157/50)
|
|
30
|
-
# Rational.call(2) # => (2/1)
|
|
31
|
-
# Rational.call(0.5) # => (1/2)
|
|
32
|
-
# @example Handle edge cases
|
|
33
|
-
# Rational.call("") # => (0/1)
|
|
34
|
-
# Rational.call(nil) # => (0/1)
|
|
35
|
-
# Rational.call(0) # => (0/1)
|
|
36
|
-
#
|
|
37
|
-
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Rational
|
|
11
|
+
# @param value [Object]
|
|
12
|
+
# @param options [Hash{Symbol => Object}]
|
|
13
|
+
# @option options [Numeric] :denominator (1)
|
|
14
|
+
# @return [Rational, Coercions::Failure]
|
|
38
15
|
def call(value, options = EMPTY_HASH)
|
|
39
|
-
Rational
|
|
16
|
+
return value if value.is_a?(::Rational)
|
|
17
|
+
|
|
18
|
+
Rational(value, options[:denominator] || 1)
|
|
40
19
|
rescue ArgumentError, FloatDomainError, RangeError, TypeError, ZeroDivisionError
|
|
41
|
-
type =
|
|
42
|
-
|
|
20
|
+
type = I18nProxy.t("cmdx.types.rational")
|
|
21
|
+
Failure.new(I18nProxy.t("cmdx.coercions.into_a", type:))
|
|
43
22
|
end
|
|
44
23
|
|
|
45
24
|
end
|
|
@@ -1,34 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
# Coerces
|
|
6
|
-
#
|
|
7
|
-
# This coercion handles various input types by converting them to their
|
|
8
|
-
# string representation. It's a simple wrapper around Ruby's String()
|
|
9
|
-
# method for consistency with the CMDx coercion interface.
|
|
4
|
+
class Coercions
|
|
5
|
+
# Coerces to String via `Kernel#String`. Never fails for normal objects.
|
|
10
6
|
module String
|
|
11
7
|
|
|
12
8
|
extend self
|
|
13
9
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# @
|
|
17
|
-
# @
|
|
18
|
-
# @option options [Object] :* Any configuration option (unused)
|
|
19
|
-
#
|
|
20
|
-
# @return [String] The coerced string value
|
|
21
|
-
#
|
|
22
|
-
# @raise [TypeError] If the value cannot be converted to a string
|
|
23
|
-
#
|
|
24
|
-
# @example Basic string coercion
|
|
25
|
-
# String.call("hello") # => "hello"
|
|
26
|
-
# String.call(42) # => "42"
|
|
27
|
-
# String.call([1, 2, 3]) # => "[1, 2, 3]"
|
|
28
|
-
# String.call(nil) # => ""
|
|
29
|
-
# String.call(true) # => "true"
|
|
30
|
-
#
|
|
31
|
-
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> String
|
|
10
|
+
# @param value [Object]
|
|
11
|
+
# @param options [Hash{Symbol => Object}]
|
|
12
|
+
# @option options [Object] reserved for future per-coercion configuration (currently ignored)
|
|
13
|
+
# @return [String]
|
|
32
14
|
def call(value, options = EMPTY_HASH)
|
|
33
15
|
String(value)
|
|
34
16
|
end
|
|
@@ -1,38 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
# Coerces
|
|
6
|
-
#
|
|
7
|
-
# This coercion handles various input types by converting them to symbols.
|
|
8
|
-
# It provides error handling for values that cannot be converted to symbols
|
|
9
|
-
# and raises appropriate CMDx coercion errors with localized messages.
|
|
4
|
+
class Coercions
|
|
5
|
+
# Coerces to Symbol via `#to_s.to_sym`. Fails only when `value` has no
|
|
6
|
+
# `#to_s` (i.e. `BasicObject` instances).
|
|
10
7
|
module Symbol
|
|
11
8
|
|
|
12
9
|
extend self
|
|
13
10
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# @
|
|
17
|
-
# @
|
|
18
|
-
# @option options [Object] :* Any configuration option (unused)
|
|
19
|
-
#
|
|
20
|
-
# @return [Symbol] The coerced symbol value
|
|
21
|
-
#
|
|
22
|
-
# @raise [CoercionError] If the value cannot be converted to a symbol
|
|
23
|
-
#
|
|
24
|
-
# @example Basic symbol coercion
|
|
25
|
-
# Symbol.call("hello") # => :hello
|
|
26
|
-
# Symbol.call("user_id") # => :user_id
|
|
27
|
-
# Symbol.call("") # => :""
|
|
28
|
-
# Symbol.call(:existing) # => :existing
|
|
29
|
-
#
|
|
30
|
-
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Symbol
|
|
11
|
+
# @param value [Object]
|
|
12
|
+
# @param options [Hash{Symbol => Object}]
|
|
13
|
+
# @option options [Object] reserved for future per-coercion configuration (currently ignored)
|
|
14
|
+
# @return [Symbol, Coercions::Failure]
|
|
31
15
|
def call(value, options = EMPTY_HASH)
|
|
32
|
-
value.
|
|
16
|
+
return value if value.is_a?(::Symbol)
|
|
17
|
+
|
|
18
|
+
value.to_s.to_sym
|
|
33
19
|
rescue NoMethodError
|
|
34
|
-
type =
|
|
35
|
-
|
|
20
|
+
type = I18nProxy.t("cmdx.types.symbol")
|
|
21
|
+
Failure.new(I18nProxy.t("cmdx.coercions.into_a", type:))
|
|
36
22
|
end
|
|
37
23
|
|
|
38
24
|
end
|
data/lib/cmdx/coercions/time.rb
CHANGED
|
@@ -1,47 +1,43 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# using Ruby's built-in time parsing methods. Supports custom strptime formats
|
|
9
|
-
# and raises CoercionError for values that cannot be converted to Time.
|
|
4
|
+
class Coercions
|
|
5
|
+
# Coerces to `Time`. Strings use `Time.parse` (or `strptime` when
|
|
6
|
+
# supplied); Numerics are treated as epoch seconds; objects responding
|
|
7
|
+
# to `#to_time` are unwrapped.
|
|
10
8
|
module Time
|
|
11
9
|
|
|
12
10
|
extend self
|
|
13
11
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# @
|
|
17
|
-
# @
|
|
18
|
-
# @option options [String] :strptime Custom strptime format string for parsing
|
|
19
|
-
#
|
|
20
|
-
# @return [Time] The converted Time object
|
|
21
|
-
#
|
|
22
|
-
# @raise [CoercionError] If the value cannot be converted to a Time object
|
|
23
|
-
#
|
|
24
|
-
# @example Convert time-like objects
|
|
25
|
-
# Time.call(Time.now) # => Time object (unchanged)
|
|
26
|
-
# Time.call(DateTime.now) # => Time object (converted)
|
|
27
|
-
# Time.call(Date.today) # => Time object (converted)
|
|
28
|
-
# @example Convert strings with default parsing
|
|
29
|
-
# Time.call("2023-12-25 10:30:00") # => Time object
|
|
30
|
-
# Time.call("2023-12-25") # => Time object
|
|
31
|
-
# Time.call("10:30:00") # => Time object
|
|
32
|
-
# @example Convert strings with custom format
|
|
33
|
-
# Time.call("25/12/2023", strptime: "%d/%m/%Y") # => Time object
|
|
34
|
-
# Time.call("12-25-2023", strptime: "%m-%d-%Y") # => Time object
|
|
35
|
-
#
|
|
36
|
-
# @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Time
|
|
12
|
+
# @param value [Object]
|
|
13
|
+
# @param options [Hash{Symbol => Object}]
|
|
14
|
+
# @option options [String] :strptime format string for `Time.strptime`
|
|
15
|
+
# @return [Time, Coercions::Failure]
|
|
37
16
|
def call(value, options = EMPTY_HASH)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
17
|
+
if value.is_a?(::Time)
|
|
18
|
+
value
|
|
19
|
+
elsif value.is_a?(::String)
|
|
20
|
+
if (strptime = options[:strptime])
|
|
21
|
+
::Time.strptime(value, strptime)
|
|
22
|
+
else
|
|
23
|
+
::Time.parse(value)
|
|
24
|
+
end
|
|
25
|
+
elsif value.is_a?(::Numeric)
|
|
26
|
+
::Time.at(value)
|
|
27
|
+
elsif value.respond_to?(:to_time)
|
|
28
|
+
value.to_time
|
|
29
|
+
else
|
|
30
|
+
coercion_failure
|
|
31
|
+
end
|
|
42
32
|
rescue ArgumentError, TypeError
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
coercion_failure
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def coercion_failure
|
|
39
|
+
type = I18nProxy.t("cmdx.types.time")
|
|
40
|
+
Failure.new(I18nProxy.t("cmdx.coercions.into_a", type:))
|
|
45
41
|
end
|
|
46
42
|
|
|
47
43
|
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
# Registry of named type coercions applied to input/output values. Ships
|
|
5
|
+
# with built-ins for `:array`, `:big_decimal`, `:boolean`, `:complex`,
|
|
6
|
+
# `:date`, `:date_time`, `:float`, `:hash`, `:integer`, `:rational`,
|
|
7
|
+
# `:string`, `:symbol`, `:time`. Coercion handlers return the coerced
|
|
8
|
+
# value on success, or a {Failure} carrying an i18n message on failure.
|
|
9
|
+
class Coercions
|
|
10
|
+
|
|
11
|
+
# Sentinel returned by a coercion when the value can't be converted.
|
|
12
|
+
# Runtime records the message as a validation error against the attribute.
|
|
13
|
+
Failure = Data.define(:message)
|
|
14
|
+
|
|
15
|
+
attr_reader :registry
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@registry = {
|
|
19
|
+
array: Coercions::Array,
|
|
20
|
+
big_decimal: Coercions::BigDecimal,
|
|
21
|
+
boolean: Coercions::Boolean,
|
|
22
|
+
complex: Coercions::Complex,
|
|
23
|
+
date: Coercions::Date,
|
|
24
|
+
date_time: Coercions::DateTime,
|
|
25
|
+
float: Coercions::Float,
|
|
26
|
+
hash: Coercions::Hash,
|
|
27
|
+
integer: Coercions::Integer,
|
|
28
|
+
rational: Coercions::Rational,
|
|
29
|
+
string: Coercions::String,
|
|
30
|
+
symbol: Coercions::Symbol,
|
|
31
|
+
time: Coercions::Time
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param source [Coercions] registry to duplicate
|
|
36
|
+
# @return [void]
|
|
37
|
+
def initialize_copy(source)
|
|
38
|
+
@registry = source.registry.dup
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Registers a named coercion, overwriting any existing entry with the
|
|
42
|
+
# same name.
|
|
43
|
+
#
|
|
44
|
+
# @param name [Symbol]
|
|
45
|
+
# @param callable [#call, nil] pass either this or a block
|
|
46
|
+
# @param block [#call, nil] coercion implementation when `callable` is omitted
|
|
47
|
+
# @yield (see built-in coercion signatures — `call(value, options = {})`)
|
|
48
|
+
# @return [Coercions] self for chaining
|
|
49
|
+
# @raise [ArgumentError] when both `callable` and a block are given, or
|
|
50
|
+
# when the resolved coercion isn't callable
|
|
51
|
+
def register(name, callable = nil, &block)
|
|
52
|
+
coercion = callable || block
|
|
53
|
+
|
|
54
|
+
if callable && block
|
|
55
|
+
raise ArgumentError, "provide either a callable or a block, not both"
|
|
56
|
+
elsif !coercion.respond_to?(:call)
|
|
57
|
+
raise ArgumentError, "coercion must respond to #call"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
registry[name.to_sym] = coercion
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param name [Symbol]
|
|
65
|
+
# @return [Coercions] self for chaining
|
|
66
|
+
def deregister(name)
|
|
67
|
+
registry.delete(name.to_sym)
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @param name [Symbol]
|
|
72
|
+
# @return [#call] the registered coercion
|
|
73
|
+
# @raise [ArgumentError] when `name` isn't registered
|
|
74
|
+
def lookup(name)
|
|
75
|
+
registry[name] || begin
|
|
76
|
+
raise ArgumentError, "unknown coercion: #{name}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Normalizes the `:coerce` declaration on an input/output into a list of
|
|
81
|
+
# `[handler, opts]` pairs. Accepts a Symbol, Array, Hash, or any
|
|
82
|
+
# callable.
|
|
83
|
+
#
|
|
84
|
+
# @param options [Hash{Symbol => Object}] declaration options
|
|
85
|
+
# @option options [Object] :coerce coercion rule(s): Symbol, Array, Hash, or `#call`-able
|
|
86
|
+
# @return [Array<Array(Object, Hash)>] pairs of handler + per-handler options
|
|
87
|
+
# @raise [ArgumentError] when `:coerce` is an unsupported format
|
|
88
|
+
def extract(options)
|
|
89
|
+
return EMPTY_ARRAY if options.empty?
|
|
90
|
+
|
|
91
|
+
raw = options[:coerce]
|
|
92
|
+
return EMPTY_ARRAY if raw.nil? || raw == EMPTY_ARRAY
|
|
93
|
+
|
|
94
|
+
case raw
|
|
95
|
+
when ::Symbol
|
|
96
|
+
[[raw, EMPTY_HASH]]
|
|
97
|
+
when ::Array
|
|
98
|
+
raw.map { |t| normalize_entry(t) }
|
|
99
|
+
when ::Hash
|
|
100
|
+
raw.map { |k, v| [k, v == true ? EMPTY_HASH : v] }
|
|
101
|
+
else
|
|
102
|
+
return [[raw, EMPTY_HASH]] if raw.respond_to?(:call)
|
|
103
|
+
|
|
104
|
+
raise ArgumentError, "unsupported type format: #{raw.inspect}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @return [Boolean]
|
|
109
|
+
def empty?
|
|
110
|
+
registry.empty?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# @return [Integer]
|
|
114
|
+
def size
|
|
115
|
+
registry.size
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Applies each coercion rule to `value`. Returns the first successful
|
|
119
|
+
# coercion. When every rule fails and more than one was declared (and
|
|
120
|
+
# none were inline callables), the aggregated "into_any" message is
|
|
121
|
+
# recorded; otherwise the last individual failure is used.
|
|
122
|
+
#
|
|
123
|
+
# @param task [Task] used for inline `Symbol`/`Proc` handlers and error recording
|
|
124
|
+
# @param name [Symbol] attribute name for error reporting
|
|
125
|
+
# @param value [Object] raw input before coercion rules run
|
|
126
|
+
# @param rules [Array<Array(Object, Hash)>] from {#extract}
|
|
127
|
+
# @return [Object, Failure] coerced value, or `Failure` when every rule failed
|
|
128
|
+
def coerce(task, name, value, rules)
|
|
129
|
+
return value if rules.empty?
|
|
130
|
+
|
|
131
|
+
last_failure = nil
|
|
132
|
+
any_inline = false
|
|
133
|
+
|
|
134
|
+
rules.each do |handler, opts|
|
|
135
|
+
result =
|
|
136
|
+
if handler.is_a?(::Symbol) && registry.key?(handler)
|
|
137
|
+
lookup(handler).call(value, **opts)
|
|
138
|
+
else
|
|
139
|
+
any_inline = true
|
|
140
|
+
Coercions::Coerce.call(task, value, handler)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
return result unless result.is_a?(Failure)
|
|
144
|
+
|
|
145
|
+
last_failure = result
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
if rules.size > 1 && !any_inline
|
|
149
|
+
type_names = rules.map { |h, _| I18nProxy.t("cmdx.types.#{h}") }.join(", ")
|
|
150
|
+
last_failure = Failure.new(I18nProxy.t("cmdx.coercions.into_any", types: type_names))
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
task.errors.add(name, last_failure.message)
|
|
154
|
+
last_failure
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
# @param entry [Object] Array entry from a `:coerce` list
|
|
160
|
+
# @return [Array(Object, Hash)] handler + options pair
|
|
161
|
+
# @raise [ArgumentError] when `entry` is unsupported
|
|
162
|
+
def normalize_entry(entry)
|
|
163
|
+
case entry
|
|
164
|
+
when ::Symbol, ::Proc
|
|
165
|
+
[entry, EMPTY_HASH]
|
|
166
|
+
else
|
|
167
|
+
return [entry, EMPTY_HASH] if entry.respond_to?(:call)
|
|
168
|
+
|
|
169
|
+
raise ArgumentError, "unsupported coerce entry: #{entry.inspect}"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
end
|
|
174
|
+
end
|