amounts 0.0.7 → 0.0.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e8169dc3622ec4944cf900418dac855398a8f1ab1dcf59c07a7d839ace60e205
4
- data.tar.gz: 6e2d1b8adcaae026bbe777be07f615ec7f7f2c8902e90c6b59e25a0210417b4b
3
+ metadata.gz: c850c2cdba4add1e09b4d6b5bfe0d1d45ffd9002924a24f5ad59e63a9f4ca513
4
+ data.tar.gz: bd146211750f51dc54e3967a3f2937eea48334cc88f1c0480b48f199c038e7fa
5
5
  SHA512:
6
- metadata.gz: 8afe516fe8c082748e669faa6db55f780a59b8b51948780fb5c23615ac417c830227a447393f5c33393249bbe4b10b40012ee8e090109cfeaf6ca366b197c87e
7
- data.tar.gz: 984d7e60a58d97dd6b3fed6ba40845bcaa66f2e6bc72e3a2bf36e80880886743ae38217f26efd84a048c7b31d2682b11d993620bd97b6c9dd8901f92e5c29c0c
6
+ metadata.gz: ff1b3637f8a8df2b98a8c64975f1f0eef01f876bf8239b36a097624a3cc7218477da32d7dc95d26d22f11a83068dabf2306195480efc1d9f65007e01e3308ef1
7
+ data.tar.gz: 947b9a81380612ca2b0f4ccd234936fca76f479bed1be42096a45715a1cd59fb80ead76089b4cb0c8120f8bf1d8fbe3b26c88c9a9860c39265d0f2bcbc1719da
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.8 - 2026-05-05
4
+
5
+ ### Changed
6
+
7
+ - **Breaking:** Generated constructors now have an `of_` prefix:
8
+ `Amount.usdc("1.50")` becomes `Amount.of_usdc("1.50")`, `Amount.gold("1.0")`
9
+ becomes `Amount.of_gold("1.0")`, and so on. Every generated constructor on
10
+ `Amount` is renamed; only the registered symbol stays the same, so
11
+ `Amount.new("1.50", :USDC)` still works.
12
+ - The original naming was a latent footgun: any short ISO/symbol code that
13
+ happened to match a method already defined on `Object` (`try`, `then`,
14
+ `tap`, `send`, `display`, `format`, ...) would raise
15
+ `Amount::Registry::AlreadyRegistered` at boot. The first reported
16
+ manifestation was `:TRY` (Turkish Lira) under Rails, where ActiveSupport's
17
+ `Object#try` made the gem unusable for any application that needed to
18
+ denominate amounts in Turkish Lira.
19
+ - The collision check in `GeneratedConstructors#raise_collision!` is kept as
20
+ a defensive backstop, but with the `of_` prefix it should never fire in
21
+ practice.
22
+
23
+ ### Migration
24
+
25
+ Search-and-replace `Amount.<symbol>(` with `Amount.of_<symbol>(` across your
26
+ codebase. Any place that builds an `Amount` from a registered symbol uses one
27
+ of the generated constructors; everywhere else (`Amount.new`,
28
+ `Amount.parse`, `Amount.register`, `Amount.registry`, `Amount.with_registry`)
29
+ is unchanged.
30
+
3
31
  ## 0.0.7 - 2026-05-03
4
32
 
5
33
  ### Fixed
@@ -38,7 +66,7 @@
38
66
  numeric string without the `display_symbol` prefix or suffix. Useful when
39
67
  the caller renders the currency label separately (e.g. in a column header
40
68
  or chip). Composes with `unit:` and `direction:` — for example,
41
- `Amount.gold("1").ui(unit: :gram, decorated: false)` returns `"31.10"`.
69
+ `Amount.of_gold("1").ui(unit: :gram, decorated: false)` returns `"31.10"`.
42
70
  Default remains `decorated: true`, so existing callers see no change.
43
71
 
44
72
  ## 0.0.4 - 2026-04-26
data/README.md CHANGED
@@ -60,7 +60,7 @@ Amount.register :USD,
60
60
 
61
61
  Amount.register_default_rate :USD, :USDC, 1
62
62
 
63
- usdc = Amount.usdc("10.00")
63
+ usdc = Amount.of_usdc("10.00")
64
64
  usd = Amount.new("5.00", :USD)
65
65
 
66
66
  (usdc + usd).ui
@@ -90,7 +90,7 @@ Construction rules:
90
90
  - `String` defaults to UI decimal values
91
91
  - `Float`, `BigDecimal`, and `Rational` are treated as decimal UI values
92
92
  - `from: :atomic`, `:ui`, or `:float` overrides inference
93
- - registering `:USDC` also defines `Amount.usdc(...)` when the symbol is a valid Ruby method name
93
+ - registering `:USDC` also defines `Amount.of_usdc(...)` when the symbol is a valid Ruby method name
94
94
 
95
95
  ### Registry
96
96
 
@@ -173,7 +173,7 @@ remainder.atomic # => -1
173
173
  ```ruby
174
174
  Amount.new(1_500_000, :USDC)
175
175
  Amount.new("1.50", :USDC)
176
- Amount.usdc("1.50")
176
+ Amount.of_usdc("1.50")
177
177
  Amount.parse("USDC|1.50")
178
178
  Amount.load(atomic: 1_500_000, symbol: :USDC)
179
179
  ```
@@ -8,7 +8,7 @@ class Amount
8
8
  end
9
9
 
10
10
  generators do
11
- require_relative "../../../generators/amount/active_record/registry_generator"
11
+ require_relative "../../generators/amount/active_record/registry_generator"
12
12
  end
13
13
  end
14
14
  end
@@ -9,7 +9,7 @@ class Amount
9
9
  module Arithmetic
10
10
  # @return [Amount]
11
11
  # @example
12
- # Amount.usdc("-1").abs
12
+ # Amount.of_usdc("-1").abs
13
13
  # # => #<Amount USDC $1.00>
14
14
  def abs
15
15
  build(@atomic.abs)
@@ -17,7 +17,7 @@ class Amount
17
17
 
18
18
  # @return [Amount]
19
19
  # @example
20
- # -Amount.usdc("1")
20
+ # -Amount.of_usdc("1")
21
21
  # # => #<Amount USDC -$1.00>
22
22
  def -@
23
23
  build(-@atomic)
@@ -27,11 +27,11 @@ class Amount
27
27
  # @return [Amount]
28
28
  # @raise [TypeMismatch]
29
29
  # @example Same-type addition
30
- # Amount.usdc("1.50") + Amount.usdc("0.50")
30
+ # Amount.of_usdc("1.50") + Amount.of_usdc("0.50")
31
31
  #
32
32
  # @example Cross-type addition using a registered directional rate
33
33
  # Amount.register_default_rate :USD, :USDC, "1"
34
- # Amount.usdc("10.00") + Amount.new("5.00", :USD)
34
+ # Amount.of_usdc("10.00") + Amount.new("5.00", :USD)
35
35
  def +(other)
36
36
  rhs = coerce_other_to_self_type!(other)
37
37
  build(@atomic + rhs.atomic)
@@ -41,7 +41,7 @@ class Amount
41
41
  # @return [Amount]
42
42
  # @raise [TypeMismatch]
43
43
  # @example
44
- # Amount.usdc("2.00") - Amount.usdc("0.50")
44
+ # Amount.of_usdc("2.00") - Amount.of_usdc("0.50")
45
45
  def -(other)
46
46
  rhs = coerce_other_to_self_type!(other)
47
47
  build(@atomic - rhs.atomic)
@@ -51,7 +51,7 @@ class Amount
51
51
  # @return [Amount]
52
52
  # @raise [TypeMismatch]
53
53
  # @example
54
- # Amount.usdc("1.25") * 2
54
+ # Amount.of_usdc("1.25") * 2
55
55
  def *(scalar)
56
56
  ensure_scalar!(scalar)
57
57
  build((BigDecimal(@atomic) * Amount.coerce_decimal(scalar)).to_i)
@@ -61,10 +61,10 @@ class Amount
61
61
  # @return [Amount, BigDecimal]
62
62
  # @raise [TypeMismatch, ZeroDivisionError]
63
63
  # @example Dividing by a scalar returns an amount
64
- # Amount.usdc("1.00") / 2
64
+ # Amount.of_usdc("1.00") / 2
65
65
  #
66
66
  # @example Dividing by an amount returns a ratio
67
- # Amount.usdc("10.00") / Amount.usdc("2.00")
67
+ # Amount.of_usdc("10.00") / Amount.of_usdc("2.00")
68
68
  def /(other)
69
69
  if other.is_a?(Amount)
70
70
  ensure_same_type!(other)
@@ -11,7 +11,7 @@ class Amount
11
11
  # @param other [Object]
12
12
  # @return [Boolean]
13
13
  # @example
14
- # Amount.usdc("1").same_type?(Amount.usdc("2"))
14
+ # Amount.of_usdc("1").same_type?(Amount.of_usdc("2"))
15
15
  # # => true
16
16
  def same_type?(other)
17
17
  other.is_a?(Amount) && other.symbol == symbol
@@ -19,26 +19,26 @@ class Amount
19
19
 
20
20
  # @return [Boolean]
21
21
  # @example
22
- # Amount.usdc(0, from: :atomic).zero?
22
+ # Amount.of_usdc(0, from: :atomic).zero?
23
23
  # # => true
24
24
  def zero? = @atomic.zero?
25
25
 
26
26
  # @return [Boolean]
27
27
  # @example
28
- # Amount.usdc("1").positive?
28
+ # Amount.of_usdc("1").positive?
29
29
  # # => true
30
30
  def positive? = @atomic.positive?
31
31
 
32
32
  # @return [Boolean]
33
33
  # @example
34
- # Amount.usdc("-1").negative?
34
+ # Amount.of_usdc("-1").negative?
35
35
  # # => true
36
36
  def negative? = @atomic.negative?
37
37
 
38
38
  # @param other [Object]
39
39
  # @return [-1, 0, 1, nil]
40
40
  # @example
41
- # Amount.usdc("1") <=> Amount.usdc("2")
41
+ # Amount.of_usdc("1") <=> Amount.of_usdc("2")
42
42
  # # => -1
43
43
  def <=>(other)
44
44
  return nil unless other.is_a?(Amount)
@@ -52,7 +52,7 @@ class Amount
52
52
  # @param other [Object]
53
53
  # @return [Boolean]
54
54
  # @example
55
- # Amount.usdc("1.50") == Amount.usdc("1.50")
55
+ # Amount.of_usdc("1.50") == Amount.of_usdc("1.50")
56
56
  # # => true
57
57
  def ==(other)
58
58
  same_type?(other) && @atomic == other.atomic
@@ -61,7 +61,7 @@ class Amount
61
61
  # @param other [Object]
62
62
  # @return [Boolean]
63
63
  # @example Hash-key equality keeps class and symbol identity
64
- # Amount.usdc("1").eql?(Amount.usdc("1"))
64
+ # Amount.of_usdc("1").eql?(Amount.of_usdc("1"))
65
65
  # # => true
66
66
  def eql?(other)
67
67
  other.class == self.class && symbol == other.symbol && @atomic == other.atomic
@@ -69,7 +69,7 @@ class Amount
69
69
 
70
70
  # @return [Integer]
71
71
  # @example
72
- # { Amount.usdc("1") => :ok }[Amount.usdc("1")]
72
+ # { Amount.of_usdc("1") => :ok }[Amount.of_usdc("1")]
73
73
  # # => :ok
74
74
  def hash
75
75
  [self.class, symbol, @atomic].hash
@@ -12,11 +12,11 @@ class Amount
12
12
  # @return [Amount]
13
13
  # @raise [Amount::Registry::NoDefaultRate] if no explicit or registered rate is available
14
14
  # @example Using an explicit one-off rate
15
- # Amount.usdc("100").to(:GOLD, rate: "0.00042")
15
+ # Amount.of_usdc("100").to(:GOLD, rate: "0.00042")
16
16
  #
17
17
  # @example Using a registered default rate
18
18
  # Amount.register_default_rate :USDC, :USD, "1"
19
- # Amount.usdc("1.50").to(:USD)
19
+ # Amount.of_usdc("1.50").to(:USD)
20
20
  def to(target_symbol, rate: nil)
21
21
  target_symbol = target_symbol.to_sym
22
22
  return self.class.new(@atomic, symbol, from: :atomic) if target_symbol == symbol
@@ -27,11 +27,11 @@ class Amount
27
27
  # `false` overrides both.
28
28
  # @return [String]
29
29
  # @example
30
- # Amount.usdc("1.50").ui # => "$1.50"
31
- # Amount.usdc("1.50").ui(decorated: false) # => "1.50"
32
- # Amount.gold("1").ui(unit: :gram) # => "31.10 g"
33
- # Amount.gold("1").ui(unit: :gram, decorated: false) # => "31.10"
34
- # Amount.sol("2.5").ui(trim_zeros: true) # => "2.5 SOL"
30
+ # Amount.of_usdc("1.50").ui # => "$1.50"
31
+ # Amount.of_usdc("1.50").ui(decorated: false) # => "1.50"
32
+ # Amount.of_gold("1").ui(unit: :gram) # => "31.10 g"
33
+ # Amount.of_gold("1").ui(unit: :gram, decorated: false) # => "31.10"
34
+ # Amount.of_sol("2.5").ui(trim_zeros: true) # => "2.5 SOL"
35
35
  def ui(unit: nil, direction: :floor, decorated: true, trim_zeros: nil)
36
36
  if unit
37
37
  render_display_unit(unit, direction, decorated:, trim_zeros:)
@@ -2,9 +2,14 @@
2
2
 
3
3
  class Amount
4
4
  class Registry
5
- # Manages registry-driven constructor methods such as `Amount.usdc(...)`.
5
+ # Manages registry-driven constructor methods such as `Amount.of_usdc(...)`.
6
+ #
7
+ # Generated names are prefixed with `of_` so that short ISO codes like
8
+ # `:TRY` (which collides with `Object#try` once ActiveSupport is loaded)
9
+ # do not clash with existing methods on `Amount`.
6
10
  class GeneratedConstructors
7
- METHOD_NAME_PATTERN = /\A[a-z_][a-z0-9_]*\z/
11
+ SYMBOL_PATTERN = /\A[a-z_][a-z0-9_]*\z/
12
+ METHOD_NAME_PREFIX = "of_"
8
13
 
9
14
  def initialize(target: Amount)
10
15
  @target = target
@@ -40,10 +45,10 @@ class Amount
40
45
  private
41
46
 
42
47
  def method_name_for(symbol)
43
- method_name = symbol.to_s.downcase
44
- return unless method_name.match?(METHOD_NAME_PATTERN)
48
+ downcased = symbol.to_s.downcase
49
+ return unless downcased.match?(SYMBOL_PATTERN)
45
50
 
46
- method_name
51
+ "#{METHOD_NAME_PREFIX}#{downcased}"
47
52
  end
48
53
 
49
54
  def raise_collision!(method_name)
@@ -51,8 +51,9 @@ class Amount
51
51
  # Registers a new fungible type.
52
52
  #
53
53
  # When the symbol is a valid Ruby method name after downcasing, an
54
- # ergonomic constructor is also generated on `Amount`, such as
55
- # `Amount.usdc("1.50")`.
54
+ # ergonomic constructor is also generated on `Amount` with an `of_`
55
+ # prefix, such as `Amount.of_usdc("1.50")`. The prefix avoids collisions
56
+ # with existing methods like `Object#try` (added by ActiveSupport).
56
57
  #
57
58
  # @param symbol [Symbol, String] registered type identifier
58
59
  # @param decimals [Integer] number of storage decimals
data/lib/amount/rspec.rb CHANGED
@@ -12,10 +12,10 @@ require_relative "rspec/matchers"
12
12
  # @example
13
13
  # require "amount/rspec"
14
14
  #
15
- # expect(Amount.usdc("1.50")).to eq_amount("USDC|1.50")
16
- # expect(Amount.usdc("1.50")).to be_amount_of(:USDC)
17
- # expect(Amount.usdc("1.50")).to be_positive_amount
18
- # expect(Amount.usdc("1.55")).to be_approximately_amount(:USDC, "1.50", within: "0.10")
15
+ # expect(Amount.of_usdc("1.50")).to eq_amount("USDC|1.50")
16
+ # expect(Amount.of_usdc("1.50")).to be_amount_of(:USDC)
17
+ # expect(Amount.of_usdc("1.50")).to be_positive_amount
18
+ # expect(Amount.of_usdc("1.55")).to be_approximately_amount(:USDC, "1.50", within: "0.10")
19
19
  Amount::RSpec::Matchers.define_amount_equality_matcher(:eq_amount) do |*expected_arguments|
20
20
  Amount::RSpec::Support.coerce_amount_arguments(expected_arguments)
21
21
  end
@@ -41,7 +41,7 @@ class Amount
41
41
 
42
42
  # @return [Hash]
43
43
  # @example
44
- # Amount.usdc("1.50").to_h
44
+ # Amount.of_usdc("1.50").to_h
45
45
  # # => { v: 1, atomic: "1500000", symbol: "USDC" }
46
46
  def to_h
47
47
  {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Amount
4
- VERSION = "0.0.7"
4
+ VERSION = "0.0.8"
5
5
  end
data/lib/amount.rb CHANGED
@@ -30,11 +30,11 @@ require_relative "amount/serialization"
30
30
  # @example Constructing from a UI value
31
31
  # Amount.register :USDC, decimals: 6
32
32
  #
33
- # Amount.usdc("1.50").atomic
33
+ # Amount.of_usdc("1.50").atomic
34
34
  # # => 1500000
35
35
  #
36
36
  # @example Constructing from an atomic value
37
- # Amount.usdc(1_500_000, from: :atomic).decimal.to_s("F")
37
+ # Amount.of_usdc(1_500_000, from: :atomic).decimal.to_s("F")
38
38
  # # => "1.5"
39
39
  class Amount
40
40
  include Arithmetic
@@ -200,7 +200,7 @@ class Amount
200
200
 
201
201
  # @return [Amount::Registry::Entry]
202
202
  # @example Accessing display configuration for this amount
203
- # Amount.usdc("1").registry_entry.ui_decimals
203
+ # Amount.of_usdc("1").registry_entry.ui_decimals
204
204
  # # => 2
205
205
  def registry_entry
206
206
  @entry
@@ -208,7 +208,7 @@ class Amount
208
208
 
209
209
  # @return [Integer]
210
210
  # @example Reading the registered storage precision
211
- # Amount.usdc("1").decimals
211
+ # Amount.of_usdc("1").decimals
212
212
  # # => 6
213
213
  def decimals
214
214
  @entry.decimals
@@ -216,7 +216,7 @@ class Amount
216
216
 
217
217
  # @return [BigDecimal]
218
218
  # @example Converting the atomic value back to a decimal quantity
219
- # Amount.usdc(1_500_000, from: :atomic).decimal.to_s("F")
219
+ # Amount.of_usdc(1_500_000, from: :atomic).decimal.to_s("F")
220
220
  # # => "1.5"
221
221
  def decimal
222
222
  BigDecimal(@atomic) / (BigDecimal(10)**decimals)
@@ -226,7 +226,7 @@ class Amount
226
226
 
227
227
  # @return [String]
228
228
  # @example Console-friendly inspection
229
- # Amount.usdc("1.50").inspect
229
+ # Amount.of_usdc("1.50").inspect
230
230
  # # => "#<Amount USDC $1.50>"
231
231
  def inspect
232
232
  "#<#{self.class} #{symbol} #{ui}>"
data/test/test_amount.rb CHANGED
@@ -66,31 +66,39 @@ class AmountTest < Minitest::Test
66
66
  end
67
67
 
68
68
  def test_generated_constructor_matches_new
69
- assert_equal Amount.new("1.50", :USDC), Amount.usdc("1.50")
69
+ assert_equal Amount.new("1.50", :USDC), Amount.of_usdc("1.50")
70
70
  end
71
71
 
72
72
  def test_generated_constructor_accepts_from_keyword
73
- assert_equal 1_500_000, Amount.usdc(1_500_000, from: :atomic).atomic
73
+ assert_equal 1_500_000, Amount.of_usdc(1_500_000, from: :atomic).atomic
74
74
  end
75
75
 
76
76
  def test_multi_word_symbol_generates_constructor
77
77
  Amount.register :OIL_WTI_BBL, decimals: 4
78
78
 
79
- assert_equal Amount.new("1.5", :OIL_WTI_BBL), Amount.oil_wti_bbl("1.5")
79
+ assert_equal Amount.new("1.5", :OIL_WTI_BBL), Amount.of_oil_wti_bbl("1.5")
80
80
  end
81
81
 
82
82
  def test_non_method_safe_symbol_skips_constructor_generation
83
83
  Amount.register :'USDC.e', decimals: 6
84
84
 
85
- refute Amount.respond_to?(:usdc_e)
85
+ refute Amount.respond_to?(:of_usdc_e)
86
+ end
87
+
88
+ def test_generated_constructor_uses_of_prefix
89
+ # Without the `of_` prefix the generated method would shadow
90
+ # `Object#try` once ActiveSupport is loaded — see the :TRY regression.
91
+ Amount.register :TRY, decimals: 2
92
+ assert Amount.respond_to?(:of_try)
93
+ assert_equal Amount.new("100", :TRY), Amount.of_try("100")
86
94
  end
87
95
 
88
96
  def test_clear_removes_generated_constructor_methods
89
- assert Amount.respond_to?(:usdc)
97
+ assert Amount.respond_to?(:of_usdc)
90
98
 
91
99
  Amount.registry.clear!
92
100
 
93
- refute Amount.respond_to?(:usdc)
101
+ refute Amount.respond_to?(:of_usdc)
94
102
  end
95
103
 
96
104
  def test_registry_lock_prevents_mutation
@@ -105,9 +113,13 @@ class AmountTest < Minitest::Test
105
113
  end
106
114
 
107
115
  def test_constructor_collision_raises_at_registration_time
116
+ Amount.singleton_class.send(:define_method, :of_collide) { :existing }
117
+
108
118
  assert_raises(Amount::Registry::AlreadyRegistered) do
109
- Amount.register :NEW, decimals: 2
119
+ Amount.register :COLLIDE, decimals: 2
110
120
  end
121
+ ensure
122
+ Amount.singleton_class.send(:remove_method, :of_collide) if Amount.singleton_class.method_defined?(:of_collide)
111
123
  end
112
124
 
113
125
  def test_inspect_uses_default_ui_representation
@@ -617,7 +629,7 @@ class AmountCustomClassTest < Minitest::Test
617
629
  end
618
630
 
619
631
  def test_generated_constructor_returns_subclass
620
- gold = Amount.gold("1", from: :ui)
632
+ gold = Amount.of_gold("1", from: :ui)
621
633
 
622
634
  assert_instance_of GoldAmount, gold
623
635
  assert_equal "24k", gold.purity_estimate
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amounts
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Seb Scholl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-02 00:00:00.000000000 Z
11
+ date: 2026-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal