cmdx 1.8.0 → 1.9.1

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/prompts/docs.md +3 -3
  4. data/.cursor/prompts/llms.md +1 -3
  5. data/.cursor/prompts/yardoc.md +1 -0
  6. data/.irbrc +14 -2
  7. data/CHANGELOG.md +64 -45
  8. data/LLM.md +159 -53
  9. data/README.md +26 -83
  10. data/docs/.DS_Store +0 -0
  11. data/docs/assets/favicon.ico +0 -0
  12. data/docs/assets/favicon.svg +1 -0
  13. data/docs/attributes/coercions.md +12 -24
  14. data/docs/attributes/defaults.md +3 -16
  15. data/docs/attributes/definitions.md +16 -30
  16. data/docs/attributes/naming.md +3 -13
  17. data/docs/attributes/transformations.md +63 -0
  18. data/docs/attributes/validations.md +14 -33
  19. data/docs/basics/chain.md +14 -23
  20. data/docs/basics/context.md +13 -22
  21. data/docs/basics/execution.md +8 -26
  22. data/docs/basics/setup.md +8 -19
  23. data/docs/callbacks.md +19 -32
  24. data/docs/deprecation.md +8 -25
  25. data/docs/getting_started.md +109 -76
  26. data/docs/index.md +132 -0
  27. data/docs/internationalization.md +6 -18
  28. data/docs/interruptions/exceptions.md +10 -16
  29. data/docs/interruptions/faults.md +8 -25
  30. data/docs/interruptions/halt.md +12 -27
  31. data/docs/logging.md +7 -17
  32. data/docs/middlewares.md +13 -29
  33. data/docs/outcomes/result.md +21 -38
  34. data/docs/outcomes/states.md +8 -22
  35. data/docs/outcomes/statuses.md +10 -21
  36. data/docs/stylesheets/extra.css +42 -0
  37. data/docs/tips_and_tricks.md +7 -46
  38. data/docs/workflows.md +23 -38
  39. data/examples/active_record_query_tagging.md +46 -0
  40. data/examples/paper_trail_whatdunnit.md +39 -0
  41. data/lib/cmdx/attribute.rb +88 -6
  42. data/lib/cmdx/attribute_registry.rb +20 -0
  43. data/lib/cmdx/attribute_value.rb +56 -10
  44. data/lib/cmdx/callback_registry.rb +31 -2
  45. data/lib/cmdx/chain.rb +34 -1
  46. data/lib/cmdx/coercion_registry.rb +18 -0
  47. data/lib/cmdx/coercions/array.rb +2 -0
  48. data/lib/cmdx/coercions/big_decimal.rb +3 -0
  49. data/lib/cmdx/coercions/boolean.rb +5 -0
  50. data/lib/cmdx/coercions/complex.rb +2 -0
  51. data/lib/cmdx/coercions/date.rb +4 -0
  52. data/lib/cmdx/coercions/date_time.rb +5 -0
  53. data/lib/cmdx/coercions/float.rb +2 -0
  54. data/lib/cmdx/coercions/hash.rb +4 -0
  55. data/lib/cmdx/coercions/integer.rb +2 -0
  56. data/lib/cmdx/coercions/rational.rb +2 -0
  57. data/lib/cmdx/coercions/string.rb +2 -0
  58. data/lib/cmdx/coercions/symbol.rb +2 -0
  59. data/lib/cmdx/coercions/time.rb +5 -0
  60. data/lib/cmdx/configuration.rb +119 -3
  61. data/lib/cmdx/context.rb +36 -0
  62. data/lib/cmdx/deprecator.rb +6 -3
  63. data/lib/cmdx/errors.rb +22 -0
  64. data/lib/cmdx/executor.rb +136 -7
  65. data/lib/cmdx/faults.rb +14 -0
  66. data/lib/cmdx/identifier.rb +2 -0
  67. data/lib/cmdx/locale.rb +3 -0
  68. data/lib/cmdx/log_formatters/json.rb +2 -0
  69. data/lib/cmdx/log_formatters/key_value.rb +2 -0
  70. data/lib/cmdx/log_formatters/line.rb +2 -0
  71. data/lib/cmdx/log_formatters/logstash.rb +2 -0
  72. data/lib/cmdx/log_formatters/raw.rb +2 -0
  73. data/lib/cmdx/middleware_registry.rb +20 -0
  74. data/lib/cmdx/middlewares/correlate.rb +11 -0
  75. data/lib/cmdx/middlewares/runtime.rb +4 -0
  76. data/lib/cmdx/middlewares/timeout.rb +4 -0
  77. data/lib/cmdx/pipeline.rb +24 -5
  78. data/lib/cmdx/railtie.rb +13 -0
  79. data/lib/cmdx/result.rb +133 -2
  80. data/lib/cmdx/task.rb +103 -8
  81. data/lib/cmdx/utils/call.rb +2 -0
  82. data/lib/cmdx/utils/condition.rb +3 -0
  83. data/lib/cmdx/utils/format.rb +5 -0
  84. data/lib/cmdx/validator_registry.rb +18 -0
  85. data/lib/cmdx/validators/exclusion.rb +2 -0
  86. data/lib/cmdx/validators/format.rb +2 -0
  87. data/lib/cmdx/validators/inclusion.rb +2 -0
  88. data/lib/cmdx/validators/length.rb +14 -0
  89. data/lib/cmdx/validators/numeric.rb +14 -0
  90. data/lib/cmdx/validators/presence.rb +2 -0
  91. data/lib/cmdx/version.rb +4 -1
  92. data/lib/cmdx/workflow.rb +10 -0
  93. data/lib/cmdx.rb +9 -0
  94. data/lib/generators/cmdx/locale_generator.rb +0 -1
  95. data/lib/generators/cmdx/templates/install.rb +9 -0
  96. data/mkdocs.yml +122 -0
  97. data/src/cmdx-dark-logo.png +0 -0
  98. data/src/cmdx-favicon.svg +1 -0
  99. data/src/cmdx-light-logo.png +0 -0
  100. data/src/cmdx-logo.svg +1 -0
  101. metadata +14 -3
  102. data/lib/cmdx/freezer.rb +0 -51
  103. data/src/cmdx-logo.png +0 -0
@@ -6,6 +6,14 @@ module CMDx
6
6
  # in a hierarchical structure, supporting nested attribute definitions.
7
7
  class AttributeRegistry
8
8
 
9
+ # Returns the collection of registered attributes.
10
+ #
11
+ # @return [Array<Attribute>] Array of registered attributes
12
+ #
13
+ # @example
14
+ # registry.registry # => [#<Attribute @name=:name>, #<Attribute @name=:email>]
15
+ #
16
+ # @rbs @registry: Array[Attribute]
9
17
  attr_reader :registry
10
18
  alias to_a registry
11
19
 
@@ -18,6 +26,8 @@ module CMDx
18
26
  # @example
19
27
  # registry = AttributeRegistry.new
20
28
  # registry = AttributeRegistry.new([attr1, attr2])
29
+ #
30
+ # @rbs (?Array[Attribute] registry) -> void
21
31
  def initialize(registry = [])
22
32
  @registry = registry
23
33
  end
@@ -28,6 +38,8 @@ module CMDx
28
38
  #
29
39
  # @example
30
40
  # new_registry = registry.dup
41
+ #
42
+ # @rbs () -> AttributeRegistry
31
43
  def dup
32
44
  self.class.new(registry.dup)
33
45
  end
@@ -41,6 +53,8 @@ module CMDx
41
53
  # @example
42
54
  # registry.register(attribute)
43
55
  # registry.register([attr1, attr2])
56
+ #
57
+ # @rbs (Attribute | Array[Attribute] attributes) -> self
44
58
  def register(attributes)
45
59
  @registry.concat(Array(attributes))
46
60
  self
@@ -56,6 +70,8 @@ module CMDx
56
70
  # @example
57
71
  # registry.deregister(:name)
58
72
  # registry.deregister(['name1', 'name2'])
73
+ #
74
+ # @rbs ((Symbol | String | Array[Symbol | String]) names) -> self
59
75
  def deregister(names)
60
76
  Array(names).each do |name|
61
77
  @registry.reject! { |attribute| matches_attribute_tree?(attribute, name.to_sym) }
@@ -69,6 +85,8 @@ module CMDx
69
85
  # and validate the attribute hierarchy.
70
86
  #
71
87
  # @param task [Task] The task to associate with all attributes
88
+ #
89
+ # @rbs (Task task) -> void
72
90
  def define_and_verify(task)
73
91
  registry.each do |attribute|
74
92
  attribute.task = task
@@ -84,6 +102,8 @@ module CMDx
84
102
  # @param name [Symbol] The name to match against
85
103
  #
86
104
  # @return [Boolean] True if the attribute or any child matches the name
105
+ #
106
+ # @rbs (Attribute attribute, Symbol name) -> bool
87
107
  def matches_attribute_tree?(attribute, name)
88
108
  return true if attribute.method_name == name
89
109
 
@@ -8,6 +8,14 @@ module CMDx
8
8
 
9
9
  extend Forwardable
10
10
 
11
+ # Returns the attribute managed by this value handler.
12
+ #
13
+ # @return [Attribute] The attribute instance
14
+ #
15
+ # @example
16
+ # attr_value.attribute.name # => :user_id
17
+ #
18
+ # @rbs @attribute: Attribute
11
19
  attr_reader :attribute
12
20
 
13
21
  def_delegators :attribute, :task, :parent, :name, :options, :types, :source, :method_name, :required?
@@ -20,6 +28,8 @@ module CMDx
20
28
  # @example
21
29
  # attr = Attribute.new(:user_id, required: true)
22
30
  # attr_value = AttributeValue.new(attr)
31
+ #
32
+ # @rbs (Attribute attribute) -> void
23
33
  def initialize(attribute)
24
34
  @attribute = attribute
25
35
  end
@@ -30,6 +40,8 @@ module CMDx
30
40
  #
31
41
  # @example
32
42
  # attr_value.value # => "john_doe"
43
+ #
44
+ # @rbs () -> untyped
33
45
  def value
34
46
  attributes[method_name]
35
47
  end
@@ -41,6 +53,8 @@ module CMDx
41
53
  #
42
54
  # @example
43
55
  # attr_value.generate # => 42
56
+ #
57
+ # @rbs () -> untyped
44
58
  def generate
45
59
  return value if attributes.key?(method_name)
46
60
 
@@ -51,9 +65,10 @@ module CMDx
51
65
  return if errors.for?(method_name)
52
66
 
53
67
  coerced_value = coerce_value(derived_value)
68
+ transformed_value = transform_value(coerced_value)
54
69
  return if errors.for?(method_name)
55
70
 
56
- attributes[method_name] = coerced_value
71
+ attributes[method_name] = transformed_value
57
72
  end
58
73
 
59
74
  # Validates the current attribute value against configured validators.
@@ -63,6 +78,8 @@ module CMDx
63
78
  # @example
64
79
  # attr_value.validate
65
80
  # # Validates value against :presence, :format, etc.
81
+ #
82
+ # @rbs () -> void
66
83
  def validate
67
84
  registry = task.class.settings[:validators]
68
85
 
@@ -85,6 +102,7 @@ module CMDx
85
102
  # @example
86
103
  # # Sources from task method, proc, or direct value
87
104
  # source_value # => "raw_value"
105
+ # @rbs () -> untyped
88
106
  def source_value
89
107
  sourced_value =
90
108
  case source
@@ -113,7 +131,9 @@ module CMDx
113
131
  #
114
132
  # @example
115
133
  # # Default can be symbol, proc, or direct value
116
- # default_value # => "default_value"
134
+ # -> { rand(100) } # => 23
135
+ #
136
+ # @rbs () -> untyped
117
137
  def default_value
118
138
  default = options[:default]
119
139
 
@@ -138,7 +158,9 @@ module CMDx
138
158
  #
139
159
  # @example
140
160
  # # Derives from hash key, method call, or proc execution
141
- # derive_value({user_id: 42}) # => 42
161
+ # context.user_id # => 42
162
+ #
163
+ # @rbs (untyped source_value) -> untyped
142
164
  def derive_value(source_value)
143
165
  derived_value =
144
166
  case source_value
@@ -154,9 +176,31 @@ module CMDx
154
176
  nil
155
177
  end
156
178
 
179
+ # Transforms the derived value using the transform option.
180
+ #
181
+ # @param derived_value [Object] The value to transform
182
+ #
183
+ # @return [Object, nil] The transformed value or nil if transformation failed
184
+ #
185
+ # @example
186
+ # :downcase # => "hello"
187
+ #
188
+ # @rbs (untyped derived_value) -> untyped
189
+ def transform_value(derived_value)
190
+ transform = options[:transform]
191
+
192
+ if transform.is_a?(Symbol) && derived_value.respond_to?(transform, true)
193
+ derived_value.send(transform)
194
+ elsif transform.respond_to?(:call)
195
+ transform.call(derived_value)
196
+ else
197
+ derived_value
198
+ end
199
+ end
200
+
157
201
  # Coerces the derived value to the expected type(s) using the coercion registry.
158
202
  #
159
- # @param derived_value [Object] The value to coerce
203
+ # @param transformed_value [Object] The value to coerce
160
204
  #
161
205
  # @return [Object, nil] The coerced value or nil if coercion failed
162
206
  #
@@ -165,14 +209,16 @@ module CMDx
165
209
  # @example
166
210
  # # Coerces "42" to Integer, "true" to Boolean, etc.
167
211
  # coerce_value("42") # => 42
168
- def coerce_value(derived_value)
169
- return derived_value if attribute.types.empty?
212
+ #
213
+ # @rbs (untyped transformed_value) -> untyped
214
+ def coerce_value(transformed_value)
215
+ return transformed_value if types.empty?
170
216
 
171
217
  registry = task.class.settings[:coercions]
172
- last_idx = attribute.types.size - 1
218
+ last_idx = types.size - 1
173
219
 
174
- attribute.types.find.with_index do |type, i|
175
- break registry.coerce(type, task, derived_value, options)
220
+ types.find.with_index do |type, i|
221
+ break registry.coerce(type, task, transformed_value, options)
176
222
  rescue CoercionError => e
177
223
  next if i != last_idx
178
224
 
@@ -180,7 +226,7 @@ module CMDx
180
226
  if last_idx.zero?
181
227
  e.message
182
228
  else
183
- tl = attribute.types.map { |t| Locale.t("cmdx.types.#{t}") }.join(", ")
229
+ tl = types.map { |t| Locale.t("cmdx.types.#{t}") }.join(", ")
184
230
  Locale.t("cmdx.coercions.into_any", types: tl)
185
231
  end
186
232
 
@@ -7,6 +7,7 @@ module CMDx
7
7
  # Each callback type represents a specific execution phase or outcome.
8
8
  class CallbackRegistry
9
9
 
10
+ # @rbs TYPES: Array[Symbol]
10
11
  TYPES = %i[
11
12
  before_validation
12
13
  before_execution
@@ -20,10 +21,20 @@ module CMDx
20
21
  on_bad
21
22
  ].freeze
22
23
 
24
+ # Returns the internal registry of callbacks organized by type.
25
+ #
26
+ # @return [Hash{Symbol => Set<Array>}] Hash mapping callback types to their registered callables
27
+ #
28
+ # @example
29
+ # registry.registry # => { before_execution: #<Set: [[[:validate], {}]]> }
30
+ #
31
+ # @rbs @registry: Hash[Symbol, Set[Array[untyped]]]
23
32
  attr_reader :registry
24
33
  alias to_h registry
25
34
 
26
35
  # @param registry [Hash] Initial registry hash, defaults to empty
36
+ #
37
+ # @rbs (?Hash[Symbol, Set[Array[untyped]]] registry) -> void
27
38
  def initialize(registry = {})
28
39
  @registry = registry
29
40
  end
@@ -31,6 +42,8 @@ module CMDx
31
42
  # Creates a deep copy of the registry with duplicated callable sets
32
43
  #
33
44
  # @return [CallbackRegistry] A new instance with duplicated registry contents
45
+ #
46
+ # @rbs () -> CallbackRegistry
34
47
  def dup
35
48
  self.class.new(registry.transform_values(&:dup))
36
49
  end
@@ -54,6 +67,8 @@ module CMDx
54
67
  # registry.register(:on_success, if: { status: :completed }) do |task|
55
68
  # task.log("Success callback executed")
56
69
  # end
70
+ #
71
+ # @rbs (Symbol type, *untyped callables, **untyped options) ?{ (Task) -> void } -> self
57
72
  def register(type, *callables, **options, &block)
58
73
  callables << block if block_given?
59
74
 
@@ -73,6 +88,8 @@ module CMDx
73
88
  #
74
89
  # @example Remove a specific callback
75
90
  # registry.deregister(:before_execution, :validate_inputs)
91
+ #
92
+ # @rbs (Symbol type, *untyped callables, **untyped options) ?{ (Task) -> void } -> self
76
93
  def deregister(type, *callables, **options, &block)
77
94
  callables << block if block_given?
78
95
  return self unless registry[type]
@@ -91,13 +108,25 @@ module CMDx
91
108
  #
92
109
  # @example Invoke all before_execution callbacks
93
110
  # registry.invoke(:before_execution, task)
111
+ #
112
+ # @rbs (Symbol type, Task task) -> void
94
113
  def invoke(type, task)
95
114
  raise TypeError, "unknown callback type #{type.inspect}" unless TYPES.include?(type)
96
115
 
97
116
  Array(registry[type]).each do |callables, options|
98
- next unless Utils::Condition.evaluate(task, options, task)
117
+ next unless Utils::Condition.evaluate(task, options)
99
118
 
100
- Array(callables).each { |callable| Utils::Call.invoke(task, callable, task) }
119
+ Array(callables).each do |callable|
120
+ if callable.is_a?(Symbol)
121
+ task.send(callable)
122
+ elsif callable.is_a?(Proc)
123
+ task.instance_exec(&callable)
124
+ elsif callable.respond_to?(:call)
125
+ callable.call(task)
126
+ else
127
+ raise "cannot invoke #{callable}"
128
+ end
129
+ end
101
130
  end
102
131
  end
103
132
 
data/lib/cmdx/chain.rb CHANGED
@@ -8,9 +8,28 @@ module CMDx
8
8
 
9
9
  extend Forwardable
10
10
 
11
+ # @rbs THREAD_KEY: Symbol
11
12
  THREAD_KEY = :cmdx_chain
12
13
 
13
- attr_reader :id, :results
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
14
33
 
15
34
  def_delegators :results, :index, :first, :last, :size
16
35
  def_delegators :first, :state, :status, :outcome, :runtime
@@ -18,6 +37,8 @@ module CMDx
18
37
  # Creates a new chain with a unique identifier and empty results collection.
19
38
  #
20
39
  # @return [Chain] A new chain instance
40
+ #
41
+ # @rbs () -> void
21
42
  def initialize
22
43
  @id = Identifier.generate
23
44
  @results = []
@@ -34,6 +55,8 @@ module CMDx
34
55
  # if chain
35
56
  # puts "Current chain: #{chain.id}"
36
57
  # end
58
+ #
59
+ # @rbs () -> Chain?
37
60
  def current
38
61
  Thread.current[THREAD_KEY]
39
62
  end
@@ -46,6 +69,8 @@ module CMDx
46
69
  #
47
70
  # @example
48
71
  # Chain.current = my_chain
72
+ #
73
+ # @rbs (Chain chain) -> Chain
49
74
  def current=(chain)
50
75
  Thread.current[THREAD_KEY] = chain
51
76
  end
@@ -56,6 +81,8 @@ module CMDx
56
81
  #
57
82
  # @example
58
83
  # Chain.clear
84
+ #
85
+ # @rbs () -> nil
59
86
  def clear
60
87
  Thread.current[THREAD_KEY] = nil
61
88
  end
@@ -73,6 +100,8 @@ module CMDx
73
100
  # result = task.execute
74
101
  # chain = Chain.build(result)
75
102
  # puts "Chain size: #{chain.size}"
103
+ #
104
+ # @rbs (Result result) -> Chain
76
105
  def build(result)
77
106
  raise TypeError, "must be a CMDx::Result" unless result.is_a?(Result)
78
107
 
@@ -95,6 +124,8 @@ module CMDx
95
124
  # chain_hash = chain.to_h
96
125
  # puts chain_hash[:id]
97
126
  # puts chain_hash[:results].size
127
+ #
128
+ # @rbs () -> Hash[Symbol, untyped]
98
129
  def to_h
99
130
  {
100
131
  id: id,
@@ -108,6 +139,8 @@ module CMDx
108
139
  #
109
140
  # @example
110
141
  # puts chain.to_s
142
+ #
143
+ # @rbs () -> String
111
144
  def to_s
112
145
  Utils::Format.to_str(to_h)
113
146
  end
@@ -7,6 +7,14 @@ module CMDx
7
7
  # for various data types including arrays, numbers, dates, and other primitives.
8
8
  class CoercionRegistry
9
9
 
10
+ # Returns the internal registry mapping coercion types to handler classes.
11
+ #
12
+ # @return [Hash{Symbol => Class}] Hash of coercion type names to coercion classes
13
+ #
14
+ # @example
15
+ # registry.registry # => { integer: Coercions::Integer, boolean: Coercions::Boolean }
16
+ #
17
+ # @rbs @registry: Hash[Symbol, Class]
10
18
  attr_reader :registry
11
19
  alias to_h registry
12
20
 
@@ -17,6 +25,8 @@ module CMDx
17
25
  # @example
18
26
  # registry = CoercionRegistry.new
19
27
  # registry = CoercionRegistry.new(custom: CustomCoercion)
28
+ #
29
+ # @rbs (?Hash[Symbol, Class]? registry) -> void
20
30
  def initialize(registry = nil)
21
31
  @registry = registry || {
22
32
  array: Coercions::Array,
@@ -40,6 +50,8 @@ module CMDx
40
50
  #
41
51
  # @example
42
52
  # new_registry = registry.dup
53
+ #
54
+ # @rbs () -> CoercionRegistry
43
55
  def dup
44
56
  self.class.new(registry.dup)
45
57
  end
@@ -54,6 +66,8 @@ module CMDx
54
66
  # @example
55
67
  # registry.register(:custom_type, CustomCoercion)
56
68
  # registry.register("another_type", AnotherCoercion)
69
+ #
70
+ # @rbs ((Symbol | String) name, Class coercion) -> self
57
71
  def register(name, coercion)
58
72
  registry[name.to_sym] = coercion
59
73
  self
@@ -68,6 +82,8 @@ module CMDx
68
82
  # @example
69
83
  # registry.deregister(:custom_type)
70
84
  # registry.deregister("another_type")
85
+ #
86
+ # @rbs ((Symbol | String) name) -> self
71
87
  def deregister(name)
72
88
  registry.delete(name.to_sym)
73
89
  self
@@ -87,6 +103,8 @@ module CMDx
87
103
  # @example
88
104
  # result = registry.coerce(:integer, task, "42")
89
105
  # result = registry.coerce(:boolean, task, "true", strict: true)
106
+ #
107
+ # @rbs (Symbol type, untyped task, untyped value, ?Hash[Symbol, untyped] options) -> untyped
90
108
  def coerce(type, task, value, options = {})
91
109
  raise TypeError, "unknown coercion type #{type.inspect}" unless registry.key?(type)
92
110
 
@@ -26,6 +26,8 @@ module CMDx
26
26
  # Array.call("hello") # => ["hello"]
27
27
  # Array.call(42) # => [42]
28
28
  # Array.call(nil) # => []
29
+ #
30
+ # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Array[untyped]
29
31
  def call(value, options = {})
30
32
  if value.is_a?(::String) && value.start_with?("[")
31
33
  JSON.parse(value)
@@ -10,6 +10,7 @@ module CMDx
10
10
 
11
11
  extend self
12
12
 
13
+ # @rbs DEFAULT_PRECISION: Integer
13
14
  DEFAULT_PRECISION = 14
14
15
 
15
16
  # Converts a value to a BigDecimal
@@ -28,6 +29,8 @@ module CMDx
28
29
  # @example Convert other numeric types
29
30
  # BigDecimal.call(42) # => #<BigDecimal:7f8b8c0d8e0f '0.42E2',9(18)>
30
31
  # BigDecimal.call(3.14159) # => #<BigDecimal:7f8b8c0d8e0f '0.314159E1',9(18)>
32
+ #
33
+ # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> BigDecimal
31
34
  def call(value, options = {})
32
35
  BigDecimal(value, options[:precision] || DEFAULT_PRECISION)
33
36
  rescue ArgumentError, TypeError
@@ -10,7 +10,10 @@ module CMDx
10
10
 
11
11
  extend self
12
12
 
13
+ # @rbs FALSEY: Regexp
13
14
  FALSEY = /^(false|f|no|n|0)$/i
15
+
16
+ # @rbs TRUTHY: Regexp
14
17
  TRUTHY = /^(true|t|yes|y|1)$/i
15
18
 
16
19
  # Converts a value to a Boolean
@@ -34,6 +37,8 @@ module CMDx
34
37
  # @example Handle case-insensitive input
35
38
  # Boolean.call("TRUE") # => true
36
39
  # Boolean.call("False") # => false
40
+ #
41
+ # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> bool
37
42
  def call(value, options = {})
38
43
  case value.to_s.downcase
39
44
  when FALSEY then false
@@ -26,6 +26,8 @@ module CMDx
26
26
  # Complex.call(5) # => (5+0i)
27
27
  # Complex.call(3.14) # => (3.14+0i)
28
28
  # Complex.call(Complex(1, 2)) # => (1+2i)
29
+ #
30
+ # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Complex
29
31
  def call(value, options = {})
30
32
  Complex(value)
31
33
  rescue ArgumentError, TypeError
@@ -12,6 +12,8 @@ module CMDx
12
12
  extend self
13
13
 
14
14
  # Types that are already date-like and don't need conversion
15
+ #
16
+ # @rbs ANALOG_TYPES: Array[String]
15
17
  ANALOG_TYPES = %w[Date DateTime Time].freeze
16
18
 
17
19
  # Converts a value to a Date object
@@ -33,6 +35,8 @@ module CMDx
33
35
  # @example Return existing Date objects unchanged
34
36
  # Date.call(Date.new(2023, 12, 25)) # => #<Date: 2023-12-25>
35
37
  # Date.call(DateTime.new(2023, 12, 25)) # => #<Date: 2023-12-25>
38
+ #
39
+ # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Date
36
40
  def call(value, options = {})
37
41
  return value if ANALOG_TYPES.include?(value.class.name)
38
42
  return ::Date.strptime(value, options[:strptime]) if options[:strptime]
@@ -11,6 +11,9 @@ module CMDx
11
11
 
12
12
  extend self
13
13
 
14
+ # Types that are already date-time-like and don't need conversion
15
+ #
16
+ # @rbs ANALOG_TYPES: Array[String]
14
17
  ANALOG_TYPES = %w[Date DateTime Time].freeze
15
18
 
16
19
  # Converts a value to a DateTime
@@ -32,6 +35,8 @@ module CMDx
32
35
  # @example Convert existing date objects
33
36
  # DateTime.call(Date.new(2023, 12, 25)) # => #<DateTime: 2023-12-25T00:00:00+00:00>
34
37
  # DateTime.call(Time.new(2023, 12, 25)) # => #<DateTime: 2023-12-25T00:00:00+00:00>
38
+ #
39
+ # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> DateTime
35
40
  def call(value, options = {})
36
41
  return value if ANALOG_TYPES.include?(value.class.name)
37
42
  return ::DateTime.strptime(value, options[:strptime]) if options[:strptime]
@@ -30,6 +30,8 @@ module CMDx
30
30
  # Float.call(BigDecimal("123.456")) # => 123.456
31
31
  # Float.call(Rational(3, 4)) # => 0.75
32
32
  # Float.call(Complex(5.0, 0)) # => 5.0
33
+ #
34
+ # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Float
33
35
  def call(value, options = {})
34
36
  Float(value)
35
37
  rescue ArgumentError, RangeError, TypeError
@@ -30,6 +30,8 @@ module CMDx
30
30
  # Hash.call([:a, 1, :b, 2]) # => {a: 1, b: 2}
31
31
  # @example Coerce from JSON string
32
32
  # Hash.call('{"key": "value"}') # => {"key" => "value"}
33
+ #
34
+ # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Hash[untyped, untyped]
33
35
  def call(value, options = {})
34
36
  if value.nil?
35
37
  {}
@@ -39,6 +41,8 @@ module CMDx
39
41
  ::Hash[*value]
40
42
  elsif value.is_a?(::String) && value.start_with?("{")
41
43
  JSON.parse(value)
44
+ elsif value.respond_to?(:to_h)
45
+ value.to_h
42
46
  else
43
47
  raise_coercion_error!
44
48
  end
@@ -34,6 +34,8 @@ module CMDx
34
34
  # Integer.call(nil) # => 0
35
35
  # Integer.call(false) # => 0
36
36
  # Integer.call(true) # => 1
37
+ #
38
+ # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Integer
37
39
  def call(value, options = {})
38
40
  Integer(value)
39
41
  rescue ArgumentError, FloatDomainError, RangeError, TypeError
@@ -33,6 +33,8 @@ module CMDx
33
33
  # Rational.call("") # => (0/1)
34
34
  # Rational.call(nil) # => (0/1)
35
35
  # Rational.call(0) # => (0/1)
36
+ #
37
+ # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Rational
36
38
  def call(value, options = {})
37
39
  Rational(value)
38
40
  rescue ArgumentError, FloatDomainError, RangeError, TypeError, ZeroDivisionError
@@ -26,6 +26,8 @@ module CMDx
26
26
  # String.call([1, 2, 3]) # => "[1, 2, 3]"
27
27
  # String.call(nil) # => ""
28
28
  # String.call(true) # => "true"
29
+ #
30
+ # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> String
29
31
  def call(value, options = {})
30
32
  String(value)
31
33
  end
@@ -25,6 +25,8 @@ module CMDx
25
25
  # Symbol.call("user_id") # => :user_id
26
26
  # Symbol.call("") # => :""
27
27
  # Symbol.call(:existing) # => :existing
28
+ #
29
+ # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Symbol
28
30
  def call(value, options = {})
29
31
  value.to_sym
30
32
  rescue NoMethodError
@@ -11,6 +11,9 @@ module CMDx
11
11
 
12
12
  extend self
13
13
 
14
+ # Types that are already time-like and don't need conversion
15
+ #
16
+ # @rbs ANALOG_TYPES: Array[String]
14
17
  ANALOG_TYPES = %w[DateTime Time].freeze
15
18
 
16
19
  # Converts a value to a Time object
@@ -34,6 +37,8 @@ module CMDx
34
37
  # @example Convert strings with custom format
35
38
  # Time.call("25/12/2023", strptime: "%d/%m/%Y") # => Time object
36
39
  # Time.call("12-25-2023", strptime: "%m-%d-%Y") # => Time object
40
+ #
41
+ # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Time
37
42
  def call(value, options = {})
38
43
  return value if ANALOG_TYPES.include?(value.class.name)
39
44
  return value.to_time if value.respond_to?(:to_time)