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 +4 -4
- data/CHANGELOG.md +42 -1
- data/README.md +3 -3
- data/lib/amount/active_record/railtie.rb +1 -1
- data/lib/amount/arithmetic.rb +8 -8
- data/lib/amount/comparison.rb +8 -8
- data/lib/amount/conversion.rb +2 -2
- data/lib/amount/display.rb +7 -6
- data/lib/amount/registry/generated_constructors.rb +10 -5
- data/lib/amount/registry.rb +3 -2
- data/lib/amount/rspec.rb +4 -4
- data/lib/amount/serialization.rb +6 -1
- data/lib/amount/version.rb +1 -1
- data/lib/amount.rb +6 -6
- data/test/test_active_record.rb +4 -4
- data/test/test_amount.rb +36 -10
- metadata +2 -3
- data/LICENSE.txt +0 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c850c2cdba4add1e09b4d6b5bfe0d1d45ffd9002924a24f5ad59e63a9f4ca513
|
|
4
|
+
data.tar.gz: bd146211750f51dc54e3967a3f2937eea48334cc88f1c0480b48f199c038e7fa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
```
|
data/lib/amount/arithmetic.rb
CHANGED
|
@@ -9,7 +9,7 @@ class Amount
|
|
|
9
9
|
module Arithmetic
|
|
10
10
|
# @return [Amount]
|
|
11
11
|
# @example
|
|
12
|
-
# Amount.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
64
|
+
# Amount.of_usdc("1.00") / 2
|
|
65
65
|
#
|
|
66
66
|
# @example Dividing by an amount returns a ratio
|
|
67
|
-
# Amount.
|
|
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)
|
data/lib/amount/comparison.rb
CHANGED
|
@@ -11,7 +11,7 @@ class Amount
|
|
|
11
11
|
# @param other [Object]
|
|
12
12
|
# @return [Boolean]
|
|
13
13
|
# @example
|
|
14
|
-
# Amount.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
72
|
+
# { Amount.of_usdc("1") => :ok }[Amount.of_usdc("1")]
|
|
73
73
|
# # => :ok
|
|
74
74
|
def hash
|
|
75
75
|
[self.class, symbol, @atomic].hash
|
data/lib/amount/conversion.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
data/lib/amount/display.rb
CHANGED
|
@@ -27,11 +27,11 @@ class Amount
|
|
|
27
27
|
# `false` overrides both.
|
|
28
28
|
# @return [String]
|
|
29
29
|
# @example
|
|
30
|
-
# Amount.
|
|
31
|
-
# Amount.
|
|
32
|
-
# Amount.
|
|
33
|
-
# Amount.
|
|
34
|
-
# Amount.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
return unless
|
|
48
|
+
downcased = symbol.to_s.downcase
|
|
49
|
+
return unless downcased.match?(SYMBOL_PATTERN)
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
"#{METHOD_NAME_PREFIX}#{downcased}"
|
|
47
52
|
end
|
|
48
53
|
|
|
49
54
|
def raise_collision!(method_name)
|
data/lib/amount/registry.rb
CHANGED
|
@@ -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
|
|
55
|
-
# `Amount.
|
|
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.
|
|
16
|
-
# expect(Amount.
|
|
17
|
-
# expect(Amount.
|
|
18
|
-
# expect(Amount.
|
|
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
|
data/lib/amount/serialization.rb
CHANGED
|
@@ -41,7 +41,7 @@ class Amount
|
|
|
41
41
|
|
|
42
42
|
# @return [Hash]
|
|
43
43
|
# @example
|
|
44
|
-
# Amount.
|
|
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
|
data/lib/amount/version.rb
CHANGED
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.
|
|
33
|
+
# Amount.of_usdc("1.50").atomic
|
|
34
34
|
# # => 1500000
|
|
35
35
|
#
|
|
36
36
|
# @example Constructing from an atomic value
|
|
37
|
-
# Amount.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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_active_record.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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?(:
|
|
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?(:
|
|
97
|
+
assert Amount.respond_to?(:of_usdc)
|
|
89
98
|
|
|
90
99
|
Amount.registry.clear!
|
|
91
100
|
|
|
92
|
-
refute Amount.respond_to?(:
|
|
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 :
|
|
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.
|
|
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.
|
|
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.
|
|
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-
|
|
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.
|