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.
Files changed (195) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +131 -1
  3. data/README.md +37 -24
  4. data/lib/cmdx/.DS_Store +0 -0
  5. data/lib/cmdx/callbacks.rb +179 -0
  6. data/lib/cmdx/chain.rb +78 -175
  7. data/lib/cmdx/coercions/array.rb +19 -33
  8. data/lib/cmdx/coercions/big_decimal.rb +12 -29
  9. data/lib/cmdx/coercions/boolean.rb +25 -45
  10. data/lib/cmdx/coercions/coerce.rb +32 -0
  11. data/lib/cmdx/coercions/complex.rb +12 -27
  12. data/lib/cmdx/coercions/date.rb +29 -33
  13. data/lib/cmdx/coercions/date_time.rb +29 -33
  14. data/lib/cmdx/coercions/float.rb +8 -29
  15. data/lib/cmdx/coercions/hash.rb +17 -43
  16. data/lib/cmdx/coercions/integer.rb +8 -32
  17. data/lib/cmdx/coercions/rational.rb +12 -33
  18. data/lib/cmdx/coercions/string.rb +6 -24
  19. data/lib/cmdx/coercions/symbol.rb +12 -26
  20. data/lib/cmdx/coercions/time.rb +31 -35
  21. data/lib/cmdx/coercions.rb +174 -0
  22. data/lib/cmdx/configuration.rb +45 -225
  23. data/lib/cmdx/context.rb +263 -242
  24. data/lib/cmdx/deprecation.rb +67 -0
  25. data/lib/cmdx/deprecators/error.rb +22 -0
  26. data/lib/cmdx/deprecators/log.rb +22 -0
  27. data/lib/cmdx/deprecators/warn.rb +21 -0
  28. data/lib/cmdx/deprecators.rb +101 -0
  29. data/lib/cmdx/errors.rb +145 -79
  30. data/lib/cmdx/executors/fiber.rb +42 -0
  31. data/lib/cmdx/executors/thread.rb +36 -0
  32. data/lib/cmdx/executors.rb +95 -0
  33. data/lib/cmdx/fault.rb +85 -78
  34. data/lib/cmdx/i18n_proxy.rb +104 -0
  35. data/lib/cmdx/input.rb +294 -0
  36. data/lib/cmdx/inputs.rb +218 -0
  37. data/lib/cmdx/log_formatters/json.rb +9 -20
  38. data/lib/cmdx/log_formatters/key_value.rb +10 -21
  39. data/lib/cmdx/log_formatters/line.rb +7 -19
  40. data/lib/cmdx/log_formatters/logstash.rb +8 -21
  41. data/lib/cmdx/log_formatters/raw.rb +8 -20
  42. data/lib/cmdx/logger_proxy.rb +30 -0
  43. data/lib/cmdx/mergers/deep_merge.rb +23 -0
  44. data/lib/cmdx/mergers/last_write_wins.rb +23 -0
  45. data/lib/cmdx/mergers/no_merge.rb +20 -0
  46. data/lib/cmdx/mergers.rb +95 -0
  47. data/lib/cmdx/middlewares.rb +128 -0
  48. data/lib/cmdx/output.rb +115 -0
  49. data/lib/cmdx/outputs.rb +66 -0
  50. data/lib/cmdx/pipeline.rb +144 -131
  51. data/lib/cmdx/railtie.rb +10 -36
  52. data/lib/cmdx/result.rb +252 -473
  53. data/lib/cmdx/retriers/bounded_random.rb +24 -0
  54. data/lib/cmdx/retriers/decorrelated_jitter.rb +28 -0
  55. data/lib/cmdx/retriers/exponential.rb +23 -0
  56. data/lib/cmdx/retriers/fibonacci.rb +39 -0
  57. data/lib/cmdx/retriers/full_random.rb +23 -0
  58. data/lib/cmdx/retriers/half_random.rb +24 -0
  59. data/lib/cmdx/retriers/linear.rb +23 -0
  60. data/lib/cmdx/retriers.rb +106 -0
  61. data/lib/cmdx/retry.rb +117 -138
  62. data/lib/cmdx/runtime.rb +251 -0
  63. data/lib/cmdx/settings.rb +68 -196
  64. data/lib/cmdx/signal.rb +165 -0
  65. data/lib/cmdx/task.rb +443 -336
  66. data/lib/cmdx/telemetry.rb +108 -0
  67. data/lib/cmdx/util.rb +73 -0
  68. data/lib/cmdx/validators/absence.rb +10 -39
  69. data/lib/cmdx/validators/exclusion.rb +33 -52
  70. data/lib/cmdx/validators/format.rb +19 -49
  71. data/lib/cmdx/validators/inclusion.rb +33 -54
  72. data/lib/cmdx/validators/length.rb +125 -127
  73. data/lib/cmdx/validators/numeric.rb +123 -123
  74. data/lib/cmdx/validators/presence.rb +10 -39
  75. data/lib/cmdx/validators/validate.rb +31 -0
  76. data/lib/cmdx/validators.rb +161 -0
  77. data/lib/cmdx/version.rb +2 -4
  78. data/lib/cmdx/workflow.rb +74 -82
  79. data/lib/cmdx.rb +111 -42
  80. data/lib/generators/cmdx/install_generator.rb +7 -17
  81. data/lib/generators/cmdx/task_generator.rb +12 -29
  82. data/lib/generators/cmdx/templates/install.rb +128 -52
  83. data/lib/generators/cmdx/templates/task.rb.tt +1 -1
  84. data/lib/generators/cmdx/templates/workflow.rb.tt +1 -2
  85. data/lib/generators/cmdx/workflow_generator.rb +12 -29
  86. data/lib/locales/en.yml +9 -6
  87. data/mkdocs.yml +25 -23
  88. metadata +39 -138
  89. data/lib/cmdx/attribute.rb +0 -440
  90. data/lib/cmdx/attribute_registry.rb +0 -185
  91. data/lib/cmdx/attribute_value.rb +0 -252
  92. data/lib/cmdx/callback_registry.rb +0 -169
  93. data/lib/cmdx/coercion_registry.rb +0 -138
  94. data/lib/cmdx/deprecator.rb +0 -77
  95. data/lib/cmdx/exception.rb +0 -46
  96. data/lib/cmdx/executor.rb +0 -374
  97. data/lib/cmdx/identifier.rb +0 -30
  98. data/lib/cmdx/locale.rb +0 -78
  99. data/lib/cmdx/middleware_registry.rb +0 -148
  100. data/lib/cmdx/middlewares/correlate.rb +0 -140
  101. data/lib/cmdx/middlewares/runtime.rb +0 -62
  102. data/lib/cmdx/middlewares/timeout.rb +0 -78
  103. data/lib/cmdx/parallelizer.rb +0 -100
  104. data/lib/cmdx/utils/call.rb +0 -53
  105. data/lib/cmdx/utils/condition.rb +0 -71
  106. data/lib/cmdx/utils/format.rb +0 -82
  107. data/lib/cmdx/utils/normalize.rb +0 -52
  108. data/lib/cmdx/utils/wrap.rb +0 -38
  109. data/lib/cmdx/validator_registry.rb +0 -143
  110. data/lib/generators/cmdx/locale_generator.rb +0 -39
  111. data/lib/locales/af.yml +0 -53
  112. data/lib/locales/ar.yml +0 -53
  113. data/lib/locales/az.yml +0 -53
  114. data/lib/locales/be.yml +0 -53
  115. data/lib/locales/bg.yml +0 -53
  116. data/lib/locales/bn.yml +0 -53
  117. data/lib/locales/bs.yml +0 -53
  118. data/lib/locales/ca.yml +0 -53
  119. data/lib/locales/cnr.yml +0 -53
  120. data/lib/locales/cs.yml +0 -53
  121. data/lib/locales/cy.yml +0 -53
  122. data/lib/locales/da.yml +0 -53
  123. data/lib/locales/de.yml +0 -53
  124. data/lib/locales/dz.yml +0 -53
  125. data/lib/locales/el.yml +0 -53
  126. data/lib/locales/eo.yml +0 -53
  127. data/lib/locales/es.yml +0 -53
  128. data/lib/locales/et.yml +0 -53
  129. data/lib/locales/eu.yml +0 -53
  130. data/lib/locales/fa.yml +0 -53
  131. data/lib/locales/fi.yml +0 -53
  132. data/lib/locales/fr.yml +0 -53
  133. data/lib/locales/fy.yml +0 -53
  134. data/lib/locales/gd.yml +0 -53
  135. data/lib/locales/gl.yml +0 -53
  136. data/lib/locales/he.yml +0 -53
  137. data/lib/locales/hi.yml +0 -53
  138. data/lib/locales/hr.yml +0 -53
  139. data/lib/locales/hu.yml +0 -53
  140. data/lib/locales/hy.yml +0 -53
  141. data/lib/locales/id.yml +0 -53
  142. data/lib/locales/is.yml +0 -53
  143. data/lib/locales/it.yml +0 -53
  144. data/lib/locales/ja.yml +0 -53
  145. data/lib/locales/ka.yml +0 -53
  146. data/lib/locales/kk.yml +0 -53
  147. data/lib/locales/km.yml +0 -53
  148. data/lib/locales/kn.yml +0 -53
  149. data/lib/locales/ko.yml +0 -53
  150. data/lib/locales/lb.yml +0 -53
  151. data/lib/locales/lo.yml +0 -53
  152. data/lib/locales/lt.yml +0 -53
  153. data/lib/locales/lv.yml +0 -53
  154. data/lib/locales/mg.yml +0 -53
  155. data/lib/locales/mk.yml +0 -53
  156. data/lib/locales/ml.yml +0 -53
  157. data/lib/locales/mn.yml +0 -53
  158. data/lib/locales/mr-IN.yml +0 -53
  159. data/lib/locales/ms.yml +0 -53
  160. data/lib/locales/nb.yml +0 -53
  161. data/lib/locales/ne.yml +0 -53
  162. data/lib/locales/nl.yml +0 -53
  163. data/lib/locales/nn.yml +0 -53
  164. data/lib/locales/oc.yml +0 -53
  165. data/lib/locales/or.yml +0 -53
  166. data/lib/locales/pa.yml +0 -53
  167. data/lib/locales/pl.yml +0 -53
  168. data/lib/locales/pt.yml +0 -53
  169. data/lib/locales/rm.yml +0 -53
  170. data/lib/locales/ro.yml +0 -53
  171. data/lib/locales/ru.yml +0 -53
  172. data/lib/locales/sc.yml +0 -53
  173. data/lib/locales/sk.yml +0 -53
  174. data/lib/locales/sl.yml +0 -53
  175. data/lib/locales/sq.yml +0 -53
  176. data/lib/locales/sr.yml +0 -53
  177. data/lib/locales/st.yml +0 -53
  178. data/lib/locales/sv.yml +0 -53
  179. data/lib/locales/sw.yml +0 -53
  180. data/lib/locales/ta.yml +0 -53
  181. data/lib/locales/te.yml +0 -53
  182. data/lib/locales/th.yml +0 -53
  183. data/lib/locales/tl.yml +0 -53
  184. data/lib/locales/tr.yml +0 -53
  185. data/lib/locales/tt.yml +0 -53
  186. data/lib/locales/ug.yml +0 -53
  187. data/lib/locales/uk.yml +0 -53
  188. data/lib/locales/ur.yml +0 -53
  189. data/lib/locales/uz.yml +0 -53
  190. data/lib/locales/vi.yml +0 -53
  191. data/lib/locales/wo.yml +0 -53
  192. data/lib/locales/zh-CN.yml +0 -53
  193. data/lib/locales/zh-HK.yml +0 -53
  194. data/lib/locales/zh-TW.yml +0 -53
  195. 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
- module Coercions
5
- # Converts various input types to DateTime format
6
- #
7
- # Handles conversion from date strings, Date objects, Time objects, and other
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
- # Converts a value to a DateTime
15
- #
16
- # @param value [Object] The value to convert to DateTime
17
- # @param options [Hash] Optional configuration parameters
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
- return value.to_datetime if value.respond_to?(:to_datetime)
37
- return ::DateTime.strptime(value, options[:strptime]) if options[:strptime]
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
- ::DateTime.parse(value)
40
- rescue TypeError, ::Date::Error
41
- type = Locale.t("cmdx.types.date_time")
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
@@ -1,42 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- module Coercions
5
- # Converts various input types to Float format
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
- # Converts a value to a Float
14
- #
15
- # @param value [Object] The value to convert to a float
16
- # @param options [Hash] Optional configuration parameters (currently unused)
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 = Locale.t("cmdx.types.float")
39
- raise CoercionError, Locale.t("cmdx.coercions.into_a", type:)
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
@@ -1,67 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- module Coercions
5
- # Coerces various input types into Hash objects
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
- # Coerces a value into a Hash
19
- #
20
- # @param value [Object] The value to coerce
21
- # @param options [Hash] Additional options (currently unused)
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?(::Array)
42
- ::Hash[*value]
43
- elsif value.is_a?(::String) && (
44
- value.start_with?("{") ||
45
- value.strip == "null"
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
- raise_coercion_error!
28
+ coercion_failure
52
29
  end
53
30
  rescue ArgumentError, TypeError, JSON::ParserError
54
- raise_coercion_error!
31
+ coercion_failure
55
32
  end
56
33
 
57
34
  private
58
35
 
59
- # Raises a CoercionError with localized message
60
- #
61
- # @raise [CoercionError] Always raised with localized error message
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
- module Coercions
5
- # Converts various input types to Integer format
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
- # Converts a value to an Integer
15
- #
16
- # @param value [Object] The value to convert to an integer
17
- # @param options [Hash] Optional configuration parameters (currently unused)
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 = Locale.t("cmdx.types.integer")
42
- raise CoercionError, Locale.t("cmdx.coercions.into_an", type:)
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
- module Coercions
5
- # Converts various input types to Rational format
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
- # Converts a value to a Rational
15
- #
16
- # @param value [Object] The value to convert to a rational number
17
- # @param options [Hash] Optional configuration parameters (currently unused)
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(value)
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 = Locale.t("cmdx.types.rational")
42
- raise CoercionError, Locale.t("cmdx.coercions.into_a", type:)
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
- module Coercions
5
- # Coerces values to String type using Ruby's built-in String() method.
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
- # Coerces a value to String type.
15
- #
16
- # @param value [Object] The value to coerce to a string
17
- # @param options [Hash] Optional configuration parameters (unused in this coercion)
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
- module Coercions
5
- # Coerces values to Symbol type using Ruby's to_sym method.
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
- # Coerces a value to Symbol type.
15
- #
16
- # @param value [Object] The value to coerce to a symbol
17
- # @param options [Hash] Optional configuration parameters (unused in this coercion)
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.to_sym
16
+ return value if value.is_a?(::Symbol)
17
+
18
+ value.to_s.to_sym
33
19
  rescue NoMethodError
34
- type = Locale.t("cmdx.types.symbol")
35
- raise CoercionError, Locale.t("cmdx.coercions.into_a", type:)
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
@@ -1,47 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- module Coercions
5
- # Converts various input types to Time format
6
- #
7
- # Handles conversion from strings, dates, and other time-like objects to Time
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
- # Converts a value to a Time object
15
- #
16
- # @param value [Object] The value to convert to a Time object
17
- # @param options [Hash] Optional configuration parameters
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
- return value.to_time if value.respond_to?(:to_time)
39
- return ::Time.strptime(value, options[:strptime]) if options[:strptime]
40
-
41
- ::Time.parse(value)
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
- type = Locale.t("cmdx.types.time")
44
- raise CoercionError, Locale.t("cmdx.coercions.into_a", type:)
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