amounts 0.0.6 → 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: 01ce6cdf1c52884bc594317df4afa1debc6cfd90c1a6cbef0177e0ed8e2efe63
4
- data.tar.gz: f796211c1f43214d5f8dc6f72d63715b4c901d96c9a4bed30e05220743740fee
3
+ metadata.gz: c850c2cdba4add1e09b4d6b5bfe0d1d45ffd9002924a24f5ad59e63a9f4ca513
4
+ data.tar.gz: bd146211750f51dc54e3967a3f2937eea48334cc88f1c0480b48f199c038e7fa
5
5
  SHA512:
6
- metadata.gz: ebbf7e4ff014146a29243ae8dbd857a1833e09e6c5f3e4f124da382663ef0941346a4467a30815aa99ea0b1aa2fa9ace12c799dbf8456b63dbdf4b2f6ab6e5c1
7
- data.tar.gz: 7e177014e0ad23a42cee97c6ae29f1d5c858426b211c1f63ebb3f23871c2e5aceb06edab03feee2efe26b4cf075c3d214e967d18882d3c93aff41226157ee6b9
6
+ metadata.gz: ff1b3637f8a8df2b98a8c64975f1f0eef01f876bf8239b36a097624a3cc7218477da32d7dc95d26d22f11a83068dabf2306195480efc1d9f65007e01e3308ef1
7
+ data.tar.gz: 947b9a81380612ca2b0f4ccd234936fca76f479bed1be42096a45715a1cd59fb80ead76089b4cb0c8120f8bf1d8fbe3b26c88c9a9860c39265d0f2bcbc1719da
data/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
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
+
31
+ ## 0.0.7 - 2026-05-03
32
+
33
+ ### Fixed
34
+
35
+ - `Display#to_s` now formats the decimal value using `ui_decimals`, matching
36
+ `ui(decorated: false)`. Previously `to_s` used raw `BigDecimal#to_s("F")`
37
+ which dropped trailing zeros (`USDC|1.5` instead of `USDC|1.50`), defeating
38
+ the purpose of the compact format carrying the display-ready amount.
39
+ - `Amount#as_json` returns the compact string (`to_s`), preventing a
40
+ `SystemStackError` (stack level too deep) when calling `as_json` or `to_json`
41
+ on an Amount in Rails. The circular reference between `Display` and `Amount`
42
+ caused ActiveSupport's default recursive serialization to overflow.
43
+
3
44
  ## 0.0.6 - 2026-04-28
4
45
 
5
46
  ### Added
@@ -25,7 +66,7 @@
25
66
  numeric string without the `display_symbol` prefix or suffix. Useful when
26
67
  the caller renders the currency label separately (e.g. in a column header
27
68
  or chip). Composes with `unit:` and `direction:` — for example,
28
- `Amount.gold("1").ui(unit: :gram, decorated: false)` returns `"31.10"`.
69
+ `Amount.of_gold("1").ui(unit: :gram, decorated: false)` returns `"31.10"`.
29
70
  Default remains `decorated: true`, so existing callers see no change.
30
71
 
31
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:)
@@ -42,7 +42,8 @@ class Amount
42
42
 
43
43
  # @return [String]
44
44
  def to_s
45
- "#{@entry.symbol}|#{@amount.decimal.to_s("F")}"
45
+ rounded = round(@amount.decimal, @entry.ui_decimals, :floor)
46
+ "#{@entry.symbol}|#{format_number(rounded, @entry.ui_decimals, false)}"
46
47
  end
47
48
 
48
49
  # @param unit [Symbol]
@@ -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
  {
@@ -50,5 +50,10 @@ class Amount
50
50
  symbol: @symbol.to_s
51
51
  }
52
52
  end
53
+
54
+ # @return [String]
55
+ def as_json(*)
56
+ to_s
57
+ end
53
58
  end
54
59
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Amount
4
- VERSION = "0.0.6"
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}>"
@@ -245,14 +245,14 @@ class AmountActiveRecordTest < Minitest::Test
245
245
  holding = ValidatedHolding.new(amount: "USDC|0.00", reserve: "USDC|1.25")
246
246
 
247
247
  refute holding.valid?
248
- assert_includes holding.errors[:amount], "must be greater than USDC|0.0"
248
+ assert_includes holding.errors[:amount], "must be greater than USDC|0.00"
249
249
  end
250
250
 
251
251
  def test_amount_validator_rejects_amount_above_upper_bound
252
252
  holding = ValidatedHolding.new(amount: "USDC|1000.01", reserve: "USDC|1.25")
253
253
 
254
254
  refute holding.valid?
255
- assert_includes holding.errors[:amount], "must be less than or equal to USDC|1000.0"
255
+ assert_includes holding.errors[:amount], "must be less than or equal to USDC|1000.00"
256
256
  end
257
257
 
258
258
  def test_amount_validator_allows_nil_when_allow_nil_is_set
@@ -271,14 +271,14 @@ class AmountActiveRecordTest < Minitest::Test
271
271
  holding = ValidatedHolding.new(amount: "USDC|100.00", reserve: "USDC|1.25", fee: 0)
272
272
 
273
273
  refute holding.valid?
274
- assert_includes holding.errors[:fee], "must be greater than SOL|0.0"
274
+ assert_includes holding.errors[:fee], "must be greater than SOL|0.0000"
275
275
  end
276
276
 
277
277
  def test_amount_validator_rejects_fixed_symbol_amount_at_exclusive_upper_bound
278
278
  holding = ValidatedHolding.new(amount: "USDC|100.00", reserve: "USDC|1.25", fee: 10)
279
279
 
280
280
  refute holding.valid?
281
- assert_includes holding.errors[:fee], "must be less than SOL|10.0"
281
+ assert_includes holding.errors[:fee], "must be less than SOL|10.0000"
282
282
  end
283
283
 
284
284
  def test_amount_validator_rejects_cross_type_threshold_without_rate
data/test/test_amount.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
3
4
  require_relative "test_helper"
4
5
 
5
6
  class AmountTest < Minitest::Test
@@ -51,7 +52,7 @@ class AmountTest < Minitest::Test
51
52
  amount = Amount.parse("USDC|1.5")
52
53
 
53
54
  assert_equal 1_500_000, amount.atomic
54
- assert_equal "USDC|1.5", amount.to_s
55
+ assert_equal "USDC|1.50", amount.to_s
55
56
  end
56
57
 
57
58
  def test_parse_accepts_explicit_v1_prefix
@@ -65,31 +66,39 @@ class AmountTest < Minitest::Test
65
66
  end
66
67
 
67
68
  def test_generated_constructor_matches_new
68
- assert_equal Amount.new("1.50", :USDC), Amount.usdc("1.50")
69
+ assert_equal Amount.new("1.50", :USDC), Amount.of_usdc("1.50")
69
70
  end
70
71
 
71
72
  def test_generated_constructor_accepts_from_keyword
72
- 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
73
74
  end
74
75
 
75
76
  def test_multi_word_symbol_generates_constructor
76
77
  Amount.register :OIL_WTI_BBL, decimals: 4
77
78
 
78
- 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")
79
80
  end
80
81
 
81
82
  def test_non_method_safe_symbol_skips_constructor_generation
82
83
  Amount.register :'USDC.e', decimals: 6
83
84
 
84
- 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")
85
94
  end
86
95
 
87
96
  def test_clear_removes_generated_constructor_methods
88
- assert Amount.respond_to?(:usdc)
97
+ assert Amount.respond_to?(:of_usdc)
89
98
 
90
99
  Amount.registry.clear!
91
100
 
92
- refute Amount.respond_to?(:usdc)
101
+ refute Amount.respond_to?(:of_usdc)
93
102
  end
94
103
 
95
104
  def test_registry_lock_prevents_mutation
@@ -104,9 +113,13 @@ class AmountTest < Minitest::Test
104
113
  end
105
114
 
106
115
  def test_constructor_collision_raises_at_registration_time
116
+ Amount.singleton_class.send(:define_method, :of_collide) { :existing }
117
+
107
118
  assert_raises(Amount::Registry::AlreadyRegistered) do
108
- Amount.register :NEW, decimals: 2
119
+ Amount.register :COLLIDE, decimals: 2
109
120
  end
121
+ ensure
122
+ Amount.singleton_class.send(:remove_method, :of_collide) if Amount.singleton_class.method_defined?(:of_collide)
110
123
  end
111
124
 
112
125
  def test_inspect_uses_default_ui_representation
@@ -140,7 +153,7 @@ class AmountTest < Minitest::Test
140
153
 
141
154
  assert_equal "$1.50", amount.ui
142
155
  assert_equal "1.500000", amount.formatted
143
- assert_equal "USDC|1.5", amount.to_s
156
+ assert_equal "USDC|1.50", amount.to_s
144
157
  end
145
158
 
146
159
  def test_frozen_amount_arithmetic_returns_unfrozen_result
@@ -538,6 +551,19 @@ class AmountTest < Minitest::Test
538
551
  assert_match(/missing key/, error.message)
539
552
  end
540
553
 
554
+ def test_as_json_returns_compact_string
555
+ assert_equal "USDC|1.50", Amount.new("1.5", :USDC).as_json
556
+ end
557
+
558
+ def test_to_json_returns_quoted_compact_string
559
+ assert_equal '"USDC|1.50"', Amount.new("1.5", :USDC).to_json
560
+ end
561
+
562
+ def test_as_json_works_when_nested_in_hash
563
+ hash = { amount: Amount.new("1.5", :USDC) }
564
+ assert_equal({ "amount" => "USDC|1.50" }, JSON.parse(hash.to_json))
565
+ end
566
+
541
567
  def test_load_rejects_unknown_serialization_version
542
568
  assert_raises(Amount::InvalidInput) do
543
569
  Amount.load(v: 2, atomic: "1500000", symbol: "USDC")
@@ -603,7 +629,7 @@ class AmountCustomClassTest < Minitest::Test
603
629
  end
604
630
 
605
631
  def test_generated_constructor_returns_subclass
606
- gold = Amount.gold("1", from: :ui)
632
+ gold = Amount.of_gold("1", from: :ui)
607
633
 
608
634
  assert_instance_of GoldAmount, gold
609
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.6
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-04-28 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
@@ -39,7 +39,6 @@ files:
39
39
  - ".rubocop.yml"
40
40
  - CHANGELOG.md
41
41
  - Gemfile
42
- - LICENSE.txt
43
42
  - README.md
44
43
  - Rakefile
45
44
  - bin/console
data/LICENSE.txt DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Seb Scholl
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.