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
data/lib/cmdx/chain.rb CHANGED
@@ -1,215 +1,118 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- # Manages a collection of task execution results in a thread and fiber safe manner.
5
- # Chains provide a way to track related task executions and their outcomes
6
- # within the same execution context.
4
+ # Ordered collection of {Result}s produced by a top-level task and any nested
5
+ # tasks it triggers. A Chain is stored per-fiber so concurrent workflows
6
+ # (see Pipeline parallel strategy) each get their own. The root Runtime
7
+ # clears the chain on teardown.
7
8
  class Chain
8
9
 
9
- extend Forwardable
10
+ include Enumerable
10
11
 
11
- # @rbs CONCURRENCY_KEY: Symbol
12
- CONCURRENCY_KEY = :cmdx_chain
13
-
14
- # Returns the unique identifier for this chain.
15
- #
16
- # @return [String] The chain identifier
17
- #
18
- # @example
19
- # chain.id # => "abc123xyz"
20
- #
21
- # @rbs @id: String
22
- attr_reader :id
23
-
24
- # Returns the collection of execution results in this chain.
25
- #
26
- # @return [Array<Result>] Array of task results
27
- #
28
- # @example
29
- # chain.results # => [#<Result>, #<Result>]
30
- #
31
- # @rbs @results: Array[Result]
32
- attr_reader :results
33
-
34
- def_delegators :results, :first, :last, :size
35
- def_delegators :first, :state, :status, :outcome, :runtime
36
-
37
- # Creates a new chain with a unique identifier and empty results collection.
38
- #
39
- # @return [Chain] A new chain instance
40
- #
41
- # @rbs () -> void
42
- def initialize(dry_run: false)
43
- @mutex = Mutex.new
44
- @id = Identifier.generate
45
- @results = []
46
- @dry_run = !!dry_run
47
- end
12
+ # Fiber-local storage key used by {.current}/{.current=}/{.clear}.
13
+ STORAGE_KEY = :cmdx_chain
48
14
 
49
15
  class << self
50
16
 
51
- # Retrieves the current chain for the current execution context.
52
- #
53
- # @return [Chain, nil] The current chain or nil if none exists
54
- #
55
- # @example
56
- # chain = Chain.current
57
- # if chain
58
- # puts "Current chain: #{chain.id}"
59
- # end
60
- #
61
- # @rbs () -> Chain?
17
+ # @return [Chain, nil] the chain active on the current fiber, or nil outside execution
62
18
  def current
63
- thread_or_fiber[CONCURRENCY_KEY]
19
+ Fiber[STORAGE_KEY]
64
20
  end
65
21
 
66
- # Sets the current chain for the current execution context.
67
- #
68
- # @param chain [Chain] The chain to set as current
69
- #
70
- # @return [Chain] The set chain
71
- #
72
- # @example
73
- # Chain.current = my_chain
74
- #
75
- # @rbs (Chain chain) -> Chain
22
+ # Installs `chain` as the active chain on the current fiber.
23
+ # @param chain [Chain, nil]
24
+ # @return [Chain, nil]
76
25
  def current=(chain)
77
- thread_or_fiber[CONCURRENCY_KEY] = chain
26
+ Fiber[STORAGE_KEY] = chain
78
27
  end
79
28
 
80
- # Clears the current chain for the current execution context.
81
- #
82
- # @return [nil] Always returns nil
83
- #
84
- # @example
85
- # Chain.clear
86
- #
87
- # @rbs () -> nil
29
+ # Clears the fiber-local chain reference.
30
+ # @return [nil]
88
31
  def clear
89
- thread_or_fiber[CONCURRENCY_KEY] = nil
32
+ Fiber[STORAGE_KEY] = nil
90
33
  end
91
34
 
92
- # Builds or extends the current chain by adding a result.
93
- # Creates a new chain if none exists, otherwise appends to the current one.
94
- #
95
- # @param result [Result] The task execution result to add
96
- #
97
- # @return [Chain] The current chain (newly created or existing)
98
- #
99
- # @raise [TypeError] If result is not a CMDx::Result instance
100
- #
101
- # @example
102
- # result = task.execute
103
- # chain = Chain.build(result)
104
- # puts "Chain size: #{chain.size}"
105
- #
106
- # @rbs (Result result) -> Chain
107
- def build(result, dry_run: false)
108
- raise TypeError, "must be a CMDx::Result" unless result.is_a?(Result)
109
-
110
- self.current ||= new(dry_run:)
111
- current.push(result)
112
- current
113
- end
35
+ end
114
36
 
115
- private
116
-
117
- # Returns the thread or fiber storage for the current execution context.
118
- #
119
- # @return [Hash] The thread or fiber storage
120
- #
121
- # @rbs () -> Hash
122
- if Fiber.respond_to?(:storage)
123
- def thread_or_fiber = Fiber.storage
124
- else
125
- def thread_or_fiber = Thread.current
126
- end
37
+ attr_reader :xid, :id, :results
127
38
 
39
+ # @param xid [String, nil] external correlation id (e.g. Rails `request_id`)
40
+ # shared across every {Result} in this chain. Resolved once by Runtime
41
+ # from {Configuration#xid} when the root chain is created.
42
+ def initialize(xid = nil)
43
+ @xid = xid
44
+ @id = SecureRandom.uuid_v7
45
+ @mutex = Mutex.new
46
+ @results = []
128
47
  end
129
48
 
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
49
+ # Appends `result` to the chain. Thread-safe to support parallel pipelines.
134
50
  #
135
- # @return [Array<Result>] The updated results array
136
- #
137
- # @rbs (Result result) -> Array[Result]
51
+ # @param result [Result]
52
+ # @return [Chain] self for chaining
138
53
  def push(result)
139
- @mutex.synchronize do
140
- result.instance_variable_set(:@chain_index, @results.size)
141
- @results << result
142
- end
54
+ @mutex.synchronize { @results << result }
55
+ self
143
56
  end
57
+ alias << push
144
58
 
145
- # Thread-safe lookup of a result's position in the chain.
146
- #
147
- # @param result [Result] The result to find
59
+ # Prepends `result` to the chain. Thread-safe to support parallel pipelines.
148
60
  #
149
- # @return [Integer, nil] The zero-based index or nil if not found
150
- #
151
- # @rbs (Result result) -> Integer?
61
+ # @param result [Result]
62
+ # @return [Chain] self for chaining
63
+ def unshift(result)
64
+ @mutex.synchronize { @results.unshift(result) }
65
+ self
66
+ end
67
+
68
+ # @param result [Result]
69
+ # @return [Integer, nil] zero-based position of `result`, or nil when absent
152
70
  def index(result)
153
- @mutex.synchronize { @results.index(result) }
71
+ @results.index(result)
154
72
  end
155
73
 
156
- # Returns whether the chain is running in dry-run mode.
157
- #
158
- # @return [Boolean] Whether the chain is running in dry-run mode
159
- #
160
- # @example
161
- # chain.dry_run? # => true
162
- #
163
- # @rbs () -> bool
164
- def dry_run?
165
- !!@dry_run
74
+ # @return [Result, nil] the most recently appended result
75
+ def last
76
+ @results.last
166
77
  end
167
78
 
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
79
+ # @return [Result, nil] the root result, or nil when absent
80
+ def root
81
+ @results.find(&:root?)
180
82
  end
181
83
 
182
- # Converts the chain to a hash representation.
183
- #
184
- # @option return [String] :id The chain identifier
185
- # @option return [Array<Hash>] :results Array of result hashes
186
- #
187
- # @return [Hash] Hash containing chain id and serialized results
188
- #
189
- # @example
190
- # chain_hash = chain.to_h
191
- # puts chain_hash[:id]
192
- # puts chain_hash[:results].size
193
- #
194
- # @rbs () -> Hash[Symbol, untyped]
195
- def to_h
196
- {
197
- id:,
198
- dry_run: dry_run?,
199
- results: results.map(&:to_h)
200
- }
84
+ # @return [String, nil] the state of the root result, or nil when absent
85
+ def state
86
+ root&.state
201
87
  end
202
88
 
203
- # Converts the chain to a string representation.
204
- #
205
- # @return [String] Formatted string representation of the chain
206
- #
207
- # @example
208
- # puts chain.to_s
89
+ # @return [String, nil] the status of the root result, or nil when absent
90
+ def status
91
+ root&.status
92
+ end
93
+
94
+ # @return [Boolean]
95
+ def empty?
96
+ @results.empty?
97
+ end
98
+
99
+ # @return [Integer]
100
+ def size
101
+ @results.size
102
+ end
103
+
104
+ # @yield [Result] each result in insertion order
105
+ # @return [Enumerator, Chain]
106
+ def each(&)
107
+ @results.each(&)
108
+ end
109
+
110
+ # Freezes the chain and its results. Called by Runtime teardown.
209
111
  #
210
- # @rbs () -> String
211
- def to_s
212
- Utils::Format.to_str(to_h)
112
+ # @return [Chain] self
113
+ def freeze
114
+ @mutex.synchronize { @results.freeze }
115
+ super
213
116
  end
214
117
 
215
118
  end
@@ -1,47 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- module Coercions
5
- # Converts various input types to Array format
6
- #
7
- # Handles conversion from strings that look like JSON arrays and other
8
- # values that can be wrapped in an array using Ruby's Array() method.
4
+ class Coercions
5
+ # Coerces to Array. JSON-decodes strings; arrays pass through; objects
6
+ # responding to `#to_a` are unwrapped; everything else is wrapped.
9
7
  module Array
10
8
 
11
9
  extend self
12
10
 
13
- # Converts a value to an Array
14
- #
15
- # @param value [Object] The value to convert to an array
16
- # @param options [Hash] Optional configuration parameters (currently unused)
17
- # @option options [Object] :unused Currently no options are used
18
- #
19
- # @return [Array] The converted array value
20
- #
21
- # @raise [CoercionError] If the value cannot be converted to an array
22
- #
23
- # @example Convert a JSON-like string to an array
24
- # Array.call("[1, 2, 3]") # => [1, 2, 3]
25
- # @example Convert other values using Array()
26
- # Array.call("hello") # => ["hello"]
27
- # Array.call(42) # => [42]
28
- # Array.call(nil) # => []
29
- # @example Handle invalid JSON-like strings
30
- # Array.call("[not json") # => raises CoercionError
31
- #
32
- # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Array[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 [Array, Coercions::Failure]
33
15
  def call(value, options = EMPTY_HASH)
34
- if value.is_a?(::String) && (
35
- value.start_with?("[") ||
36
- value.strip == "null"
37
- )
38
- JSON.parse(value) || []
16
+ if value.is_a?(::Array)
17
+ value
18
+ elsif value.is_a?(::String)
19
+ result = JSON.parse(value)
20
+ result.is_a?(::Array) ? result : [value]
21
+ elsif value.respond_to?(:to_a)
22
+ value.to_a
39
23
  else
40
- Utils::Wrap.array(value)
24
+ [value]
41
25
  end
42
26
  rescue JSON::ParserError
43
- type = Locale.t("cmdx.types.array")
44
- raise CoercionError, Locale.t("cmdx.coercions.into_an", type:)
27
+ [value]
28
+ rescue TypeError
29
+ type = I18nProxy.t("cmdx.types.array")
30
+ Failure.new(I18nProxy.t("cmdx.coercions.into_an", type:))
45
31
  end
46
32
 
47
33
  end
@@ -1,41 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- module Coercions
5
- # Converts various input types to BigDecimal format
6
- #
7
- # Handles conversion from numeric strings, integers, floats, and other
8
- # values that can be converted to BigDecimal using Ruby's BigDecimal() method.
4
+ class Coercions
5
+ # Coerces to `BigDecimal`. Default precision is 14 digits; override with
6
+ # `precision:` on the declaration.
9
7
  module BigDecimal
10
8
 
11
9
  extend self
12
10
 
13
- # @rbs DEFAULT_PRECISION: Integer
14
- DEFAULT_PRECISION = 14
15
-
16
- # Converts a value to a BigDecimal
17
- #
18
- # @param value [Object] The value to convert to BigDecimal
19
- # @param options [Hash] Optional configuration parameters
20
- # @option options [Integer] :precision The precision to use (defaults to DEFAULT_PRECISION)
21
- #
22
- # @return [BigDecimal] The converted BigDecimal value
23
- #
24
- # @raise [CoercionError] If the value cannot be converted to BigDecimal
25
- #
26
- # @example Convert numeric strings to BigDecimal
27
- # BigDecimal.call("123.45") # => #<BigDecimal:7f8b8c0d8e0f '0.12345E3',9(18)>
28
- # BigDecimal.call("0.001", precision: 6) # => #<BigDecimal:7f8b8c0d8e0f '0.1E-2',9(18)>
29
- # @example Convert other numeric types
30
- # BigDecimal.call(42) # => #<BigDecimal:7f8b8c0d8e0f '0.42E2',9(18)>
31
- # BigDecimal.call(3.14159) # => #<BigDecimal:7f8b8c0d8e0f '0.314159E1',9(18)>
32
- #
33
- # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> BigDecimal
11
+ # @param value [Object]
12
+ # @param options [Hash{Symbol => Object}]
13
+ # @option options [Integer] :precision (14)
14
+ # @return [BigDecimal, Coercions::Failure]
34
15
  def call(value, options = EMPTY_HASH)
35
- BigDecimal(value, options[:precision] || DEFAULT_PRECISION)
16
+ return value if value.is_a?(BigDecimal)
17
+
18
+ BigDecimal(value, options[:precision] || 14)
36
19
  rescue ArgumentError, TypeError
37
- type = Locale.t("cmdx.types.big_decimal")
38
- raise CoercionError, Locale.t("cmdx.coercions.into_a", type:)
20
+ type = I18nProxy.t("cmdx.types.big_decimal")
21
+ Failure.new(I18nProxy.t("cmdx.coercions.into_a", type:))
39
22
  end
40
23
 
41
24
  end
@@ -1,56 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- module Coercions
5
- # Converts various input types to Boolean format
6
- #
7
- # Handles conversion from strings, numbers, and other values to boolean
8
- # using predefined truthy and falsey patterns.
4
+ class Coercions
5
+ # Coerces to Boolean by matching the string form against the {TRUTHY}
6
+ # and {FALSEY} sets (case- and whitespace-insensitive). Anything else
7
+ # (including `nil`) fails.
9
8
  module Boolean
10
9
 
11
10
  extend self
12
11
 
13
- # @rbs FALSEY: Regexp
14
- FALSEY = /\A(false|f|no|n|0)\z/i
15
-
16
- # @rbs TRUTHY: Regexp
17
- TRUTHY = /\A(true|t|yes|y|1)\z/i
18
-
19
- # Converts a value to a Boolean
20
- #
21
- # @param value [Object] The value to convert to boolean
22
- # @param options [Hash] Optional configuration parameters (currently unused)
23
- # @option options [Object] :unused Currently no options are used
24
- #
25
- # @return [Boolean] The converted boolean value
26
- #
27
- # @raise [CoercionError] If the value cannot be converted to boolean
28
- #
29
- # @example Convert truthy strings to true
30
- # Boolean.call("true") # => true
31
- # Boolean.call("yes") # => true
32
- # Boolean.call("1") # => true
33
- # @example Convert falsey strings to false
34
- # Boolean.call("false") # => false
35
- # Boolean.call("no") # => false
36
- # Boolean.call("0") # => false
37
- # Boolean.call(nil) # => false
38
- # Boolean.call("") # => false
39
- # @example Handle case-insensitive input
40
- # Boolean.call("TRUE") # => true
41
- # Boolean.call("False") # => false
42
- # @example Handle edge cases
43
- # Boolean.call("abc") # => raises CoercionError
44
- #
45
- # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> bool
12
+ TRUTHY = Set["true", "yes", "on", "y", "1", "t"].freeze
13
+ FALSEY = Set["false", "no", "off", "n", "0", "f"].freeze
14
+
15
+ # @param value [Object]
16
+ # @param options [Hash{Symbol => Object}]
17
+ # @option options [Object] reserved for future per-coercion configuration (currently ignored)
18
+ # @return [Boolean, Coercions::Failure]
46
19
  def call(value, options = EMPTY_HASH)
47
- case value.to_s
48
- when FALSEY, EMPTY_STRING then false
49
- when TRUTHY then true
50
- else
51
- type = Locale.t("cmdx.types.boolean")
52
- raise CoercionError, Locale.t("cmdx.coercions.into_a", type:)
53
- end
20
+ return coercion_failure if value.nil?
21
+
22
+ str = value.to_s.strip.downcase
23
+ return true if TRUTHY.include?(str)
24
+ return false if FALSEY.include?(str)
25
+
26
+ coercion_failure
27
+ end
28
+
29
+ private
30
+
31
+ def coercion_failure
32
+ type = I18nProxy.t("cmdx.types.boolean")
33
+ Failure.new(I18nProxy.t("cmdx.coercions.into_a", type:))
54
34
  end
55
35
 
56
36
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ class Coercions
5
+ # Invokes an inline `:coerce` handler (Symbol method name, Proc, or
6
+ # anything with `#call`). Used by {Coercions#coerce} for non-built-in
7
+ # rules.
8
+ module Coerce
9
+
10
+ extend self
11
+
12
+ # @param task [Task] receiver for Symbol/Proc handlers, also passed to callable handlers
13
+ # @param value [Object]
14
+ # @param handler [Symbol, Proc, #call]
15
+ # @return [Object] the handler's return value
16
+ # @raise [ArgumentError] when `handler` isn't a supported type
17
+ def call(task, value, handler)
18
+ case handler
19
+ when ::Symbol
20
+ task.send(handler, value)
21
+ when ::Proc
22
+ task.instance_exec(value, &handler)
23
+ else
24
+ return handler.call(value, task) if handler.respond_to?(:call)
25
+
26
+ raise ArgumentError, "coerce handler must be a Symbol, Proc, or respond to #call"
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -1,39 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- module Coercions
5
- # Converts various input types to Complex number format
6
- #
7
- # Handles conversion from numeric strings, integers, floats, and other
8
- # values that can be converted to Complex using Ruby's Complex() method.
4
+ class Coercions
5
+ # Coerces to `Complex`. Supply `imaginary:` to provide the imaginary
6
+ # component when `value` is a real-only input.
9
7
  module Complex
10
8
 
11
9
  extend self
12
10
 
13
- # Converts a value to a Complex number
14
- #
15
- # @param value [Object] The value to convert to Complex
16
- # @param options [Hash] Optional configuration parameters (currently unused)
17
- # @option options [Object] :* Any configuration option (unused)
18
- #
19
- # @return [Complex] The converted Complex number value
20
- #
21
- # @raise [CoercionError] If the value cannot be converted to Complex
22
- #
23
- # @example Convert numeric strings to Complex
24
- # Complex.call("3+4i") # => (3+4i)
25
- # Complex.call("2.5") # => (2.5+0i)
26
- # @example Convert other numeric types
27
- # Complex.call(5) # => (5+0i)
28
- # Complex.call(3.14) # => (3.14+0i)
29
- # Complex.call(Complex(1, 2)) # => (1+2i)
30
- #
31
- # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Complex
11
+ # @param value [Object]
12
+ # @param options [Hash{Symbol => Object}]
13
+ # @option options [Numeric] :imaginary (0)
14
+ # @return [Complex, Coercions::Failure]
32
15
  def call(value, options = EMPTY_HASH)
33
- Complex(value)
16
+ return value if value.is_a?(::Complex)
17
+
18
+ Complex(value, options[:imaginary] || 0)
34
19
  rescue ArgumentError, TypeError
35
- type = Locale.t("cmdx.types.complex")
36
- raise CoercionError, Locale.t("cmdx.coercions.into_a", type:)
20
+ type = I18nProxy.t("cmdx.types.complex")
21
+ Failure.new(I18nProxy.t("cmdx.coercions.into_a", type:))
37
22
  end
38
23
 
39
24
  end
@@ -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 Date format
6
- #
7
- # Handles conversion from strings, Date objects, DateTime objects, Time objects,
8
- # and other date-like values to Date objects using Ruby's built-in parsing
9
- # capabilities and optional custom format parsing.
4
+ class Coercions
5
+ # Coerces to `Date`. Pass `strptime:` to parse via a specific format;
6
+ # otherwise `Date.parse` is used for strings, and `#to_date` for any
7
+ # other responding object.
10
8
  module Date
11
9
 
12
10
  extend self
13
11
 
14
- # Converts a value to a Date object
15
- #
16
- # @param value [Object] The value to convert to a Date
17
- # @param options [Hash] Optional configuration parameters
18
- # @option options [String] :strptime Custom date format string for parsing
19
- #
20
- # @return [Date] The converted Date object
21
- #
22
- # @raise [CoercionError] If the value cannot be converted to a Date
23
- #
24
- # @example Convert string to Date using default parsing
25
- # Date.call("2023-12-25") # => #<Date: 2023-12-25>
26
- # Date.call("Dec 25, 2023") # => #<Date: 2023-12-25>
27
- # @example Convert string using custom format
28
- # Date.call("25/12/2023", strptime: "%d/%m/%Y") # => #<Date: 2023-12-25>
29
- # Date.call("12-25-2023", strptime: "%m-%d-%Y") # => #<Date: 2023-12-25>
30
- # @example Return existing Date objects unchanged
31
- # Date.call(Date.new(2023, 12, 25)) # => #<Date: 2023-12-25>
32
- # Date.call(DateTime.new(2023, 12, 25)) # => #<Date: 2023-12-25>
33
- #
34
- # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Date
12
+ # @param value [Object]
13
+ # @param options [Hash{Symbol => Object}]
14
+ # @option options [String] :strptime format string for `Date.strptime`
15
+ # @return [Date, Coercions::Failure]
35
16
  def call(value, options = EMPTY_HASH)
36
- return value.to_date if value.respond_to?(:to_date)
37
- return ::Date.strptime(value, options[:strptime]) if options[:strptime]
17
+ if value.is_a?(::Date)
18
+ value
19
+ elsif value.is_a?(::String)
20
+ if (strptime = options[:strptime])
21
+ ::Date.strptime(value, strptime)
22
+ else
23
+ ::Date.parse(value)
24
+ end
25
+ elsif value.respond_to?(:to_date)
26
+ value.to_date
27
+ else
28
+ coercion_failure
29
+ end
30
+ rescue ArgumentError, TypeError, ::Date::Error
31
+ coercion_failure
32
+ end
33
+
34
+ private
38
35
 
39
- ::Date.parse(value)
40
- rescue TypeError, ::Date::Error
41
- type = Locale.t("cmdx.types.date")
42
- raise CoercionError, Locale.t("cmdx.coercions.into_a", type:)
36
+ def coercion_failure
37
+ type = I18nProxy.t("cmdx.types.date")
38
+ Failure.new(I18nProxy.t("cmdx.coercions.into_a", type:))
43
39
  end
44
40
 
45
41
  end