amounts 0.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 92c0c2ebae750b68a79648157559ca2fe6c0c67d5892a83a31beb36e15936648
4
+ data.tar.gz: c5b8661d2b980a48a655407fdababfe8428bdd978e1fd7a6d52a6d8a554be287
5
+ SHA512:
6
+ metadata.gz: 2bc8b173facf22c287824621e15f02102cff7cb4e096789e35945a77418815b6027cf9637e0e86bcfc3c427946b1b0aa3f544b01cc338550503aa50b2fa34c7e
7
+ data.tar.gz: e4cf1297e4c987f8121d6a9f35f76bc9e668a5f22b130531022428ec0443d019f9ad7d637f098c34be26edaa78d1f105479d935765d3353ef9f42cf8199e3936
data/.rubocop.yml ADDED
@@ -0,0 +1,89 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ SuggestExtensions: false
4
+ TargetRubyVersion: 3.1
5
+ UseCache: false
6
+ Exclude:
7
+ - "vendor/**/*"
8
+ - "site/**/*"
9
+
10
+ Layout/LineLength:
11
+ Enabled: false
12
+
13
+ Metrics/BlockLength:
14
+ Enabled: false
15
+ Exclude:
16
+ - "amounts.gemspec"
17
+ - "test/**/*"
18
+
19
+ Metrics/MethodLength:
20
+ Enabled: false
21
+
22
+ Metrics/AbcSize:
23
+ Enabled: false
24
+
25
+ Metrics/CyclomaticComplexity:
26
+ Enabled: false
27
+
28
+ Metrics/PerceivedComplexity:
29
+ Enabled: false
30
+
31
+ Metrics/ClassLength:
32
+ Enabled: false
33
+
34
+ Style/Documentation:
35
+ Enabled: false
36
+
37
+ Style/StringLiterals:
38
+ Enabled: false
39
+
40
+ Style/WordArray:
41
+ Enabled: false
42
+
43
+ Layout/HashAlignment:
44
+ Enabled: false
45
+
46
+ Lint/AmbiguousBlockAssociation:
47
+ Enabled: false
48
+
49
+ Lint/RedundantRequireStatement:
50
+ Enabled: false
51
+
52
+ Naming/RescuedExceptionsVariableName:
53
+ Enabled: false
54
+
55
+ Naming/BinaryOperatorParameterName:
56
+ Enabled: false
57
+
58
+ Naming/MethodParameterName:
59
+ Enabled: false
60
+
61
+ Naming/PredicateName:
62
+ Enabled: false
63
+
64
+ Metrics/ParameterLists:
65
+ Enabled: false
66
+
67
+ Style/IfUnlessModifier:
68
+ Enabled: false
69
+
70
+ Style/RedundantParentheses:
71
+ Enabled: false
72
+
73
+ Style/StringLiteralsInInterpolation:
74
+ Enabled: false
75
+
76
+ Style/SoleNestedConditional:
77
+ Enabled: false
78
+
79
+ Style/RedundantConstantBase:
80
+ Enabled: false
81
+
82
+ Layout/SpaceAroundKeyword:
83
+ Enabled: false
84
+
85
+ Gemspec/DevelopmentDependencies:
86
+ Enabled: false
87
+
88
+ Gemspec/RequireMFA:
89
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## 0.0.1 - 2026-04-25
4
+
5
+ - Initial release.
6
+ - Added the `Amount` core value object with atomic integer storage.
7
+ - Added directional default-rate conversion for cross-type arithmetic and comparison.
8
+ - Added explicit `[parts, remainder]` semantics for `split` and `allocate`.
9
+ - Added optional ActiveRecord integration via `require "amount/active_record"`.
data/Gemfile ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "minitest", ">= 5.0", "< 6.0"
8
+ gem "rake", ">= 13.0"
9
+ gem "rspec", ">= 3.13"
10
+
11
+ group :development do
12
+ gem "irb", ">= 1.13"
13
+ end
14
+
15
+ group :quality do
16
+ gem "rubocop", "~> 1.63.0"
17
+ end
18
+
19
+ group :active_record do
20
+ gem "activerecord", ">= 7.1", "< 8.0"
21
+ gem "pg", ">= 1.5"
22
+ gem "railties", ">= 7.1", "< 8.0"
23
+ gem "sqlite3", ">= 2.0"
24
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
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.
data/README.md ADDED
@@ -0,0 +1,402 @@
1
+ # amounts
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/amounts)](https://rubygems.org/gems/amounts)
4
+ [![CI](https://github.com/zarpay/amounts/actions/workflows/ci.yml/badge.svg)](https://github.com/zarpay/amounts/actions/workflows/ci.yml)
5
+ [![Release](https://img.shields.io/github/v/release/zarpay/amounts)](https://github.com/zarpay/amounts/releases)
6
+ [![License](https://img.shields.io/github/license/zarpay/amounts)](https://github.com/zarpay/amounts/blob/main/LICENSE.txt)
7
+ [![Ruby](https://img.shields.io/gem/required-ruby-version/amounts)](https://rubygems.org/gems/amounts)
8
+
9
+ `amounts` is a Ruby gem for precise quantities of fungible things: money, crypto tokens, commodities, inventory units, points, and similar value-like amounts. It stores every value as an arbitrary-precision atomic `Integer`, keeps type identity in a registry, rejects accidental cross-type math unless an explicit directional rate exists, and offers an optional ActiveRecord adapter without making Rails part of the core runtime.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ bundle add amounts
15
+ ```
16
+
17
+ or:
18
+
19
+ ```bash
20
+ gem install amounts
21
+ ```
22
+
23
+ The library entrypoint is:
24
+
25
+ ```ruby
26
+ require "amount"
27
+ ```
28
+
29
+ Load the Rails adapter only when needed:
30
+
31
+ ```ruby
32
+ require "amount/active_record"
33
+ ```
34
+
35
+ ## Quickstart
36
+
37
+ ```ruby
38
+ require "amount"
39
+
40
+ Amount.register :USDC,
41
+ decimals: 6,
42
+ display_symbol: "$",
43
+ display_position: :prefix,
44
+ ui_decimals: 2
45
+
46
+ Amount.register :USD,
47
+ decimals: 2,
48
+ display_symbol: "$",
49
+ display_position: :prefix,
50
+ ui_decimals: 2
51
+
52
+ Amount.register_default_rate :USD, :USDC, 1
53
+
54
+ usdc = Amount.usdc("10.00")
55
+ usd = Amount.new("5.00", :USD)
56
+
57
+ (usdc + usd).ui
58
+ # => "$15.00"
59
+ ```
60
+
61
+ ## Concepts
62
+
63
+ ### Atomic vs. UI values
64
+
65
+ `Amount` stores values as atomic integers in the smallest registered unit for that type.
66
+
67
+ ```ruby
68
+ Amount.register :USDC, decimals: 6
69
+
70
+ amount = Amount.new("1.5", :USDC)
71
+ amount.atomic
72
+ # => 1500000
73
+
74
+ amount.decimal.to_s("F")
75
+ # => "1.5"
76
+ ```
77
+
78
+ Construction rules:
79
+
80
+ - `Integer` defaults to atomic units
81
+ - `String` defaults to UI decimal values
82
+ - `Float`, `BigDecimal`, and `Rational` are treated as decimal UI values
83
+ - `from: :atomic`, `:ui`, or `:float` overrides inference
84
+ - registering `:USDC` also defines `Amount.usdc(...)` when the symbol is a valid Ruby method name
85
+
86
+ ### Registry
87
+
88
+ The registry defines the full behavior of each type:
89
+
90
+ ```ruby
91
+ Amount.register :GOLD,
92
+ decimals: 8,
93
+ display_symbol: "oz t",
94
+ display_position: :suffix,
95
+ ui_decimals: 4,
96
+ display_units: {
97
+ oz_t: { scale: 1, symbol: "oz t", ui_decimals: 4 },
98
+ gram: { scale: "31.1035", symbol: "g", ui_decimals: 2 }
99
+ },
100
+ default_display: :oz_t
101
+ ```
102
+
103
+ Boot-time registry configuration can be frozen once setup is complete:
104
+
105
+ ```ruby
106
+ Amount.registry.lock!
107
+ Amount.registry.locked? # => true
108
+ ```
109
+
110
+ ### Display units are not conversions
111
+
112
+ Display units scale presentation only:
113
+
114
+ ```ruby
115
+ gold = Amount.new("1.5", :GOLD)
116
+ gold.ui(unit: :gram)
117
+ # => "46.65 g"
118
+ ```
119
+
120
+ The value is still `:GOLD`.
121
+
122
+ ### Directional conversion rates
123
+
124
+ Cross-type `+`, `-`, and `<=>` only work when the right-hand side can be converted into the left-hand side using a registered default rate.
125
+
126
+ ```ruby
127
+ Amount.register_default_rate :USD, :USDC, 1
128
+
129
+ Amount.new("10", :USDC) + Amount.new("5", :USD)
130
+ # => #<Amount USDC|15.0>
131
+ ```
132
+
133
+ Rates are directional. `:USD -> :USDC` does not imply `:USDC -> :USD`.
134
+
135
+ ### Split vs. division
136
+
137
+ Scalar division returns one scaled amount:
138
+
139
+ ```ruby
140
+ Amount.new("10", :USDC) / 2
141
+ # => #<Amount USDC|5.0>
142
+ ```
143
+
144
+ `split` and `allocate` return explicit remainders:
145
+
146
+ ```ruby
147
+ parts, remainder = Amount.new(10, :LOGS).split(3)
148
+ parts.map(&:atomic) # => [3, 3, 3]
149
+ remainder.atomic # => 1
150
+ ```
151
+
152
+ Negative values follow the same rules with rounding toward zero:
153
+
154
+ ```ruby
155
+ parts, remainder = Amount.new(-10, :LOGS).allocate([1, 1, 1])
156
+ parts.map(&:atomic) # => [-3, -3, -3]
157
+ remainder.atomic # => -1
158
+ ```
159
+
160
+ ## Core API
161
+
162
+ ### Construction
163
+
164
+ ```ruby
165
+ Amount.new(1_500_000, :USDC)
166
+ Amount.new("1.50", :USDC)
167
+ Amount.usdc("1.50")
168
+ Amount.parse("USDC|1.50")
169
+ Amount.load(atomic: 1_500_000, symbol: :USDC)
170
+ ```
171
+
172
+ ### Math
173
+
174
+ ```ruby
175
+ a = Amount.new("10", :USDC)
176
+ b = Amount.new("2", :USDC)
177
+
178
+ a + b
179
+ a - b
180
+ a * 2
181
+ a / 2
182
+ a / b
183
+ ```
184
+
185
+ `Amount * Amount` raises `Amount::TypeMismatch`.
186
+
187
+ ### Comparison
188
+
189
+ ```ruby
190
+ Amount.new("1", :USDC) < Amount.new("2", :USDC)
191
+ ```
192
+
193
+ Cross-type comparison returns `nil` when no directional rate exists.
194
+
195
+ ### Conversion
196
+
197
+ ```ruby
198
+ Amount.new("10", :USDC).to(:USD, rate: "1")
199
+ ```
200
+
201
+ ### Display
202
+
203
+ ```ruby
204
+ amount.formatted
205
+ amount.ui
206
+ amount.ui(direction: :ceil)
207
+ amount.ui(unit: :gram)
208
+ amount.in_unit(:gram)
209
+ ```
210
+
211
+ ### Serialization
212
+
213
+ ```ruby
214
+ payload = amount.to_h
215
+ Amount.load(payload)
216
+ ```
217
+
218
+ ## Registry API
219
+
220
+ ```ruby
221
+ Amount.register(...)
222
+ Amount.register_default_rate(:USD, :USDC, "1")
223
+ Amount.registry.lookup(:USDC)
224
+ Amount.registry.symbols
225
+ Amount.registry.clear!
226
+ Amount.registry.lock!
227
+ Amount.registry.locked?
228
+ ```
229
+
230
+ ## ActiveRecord Integration
231
+
232
+ Load the adapter explicitly:
233
+
234
+ ```ruby
235
+ require "amount/active_record"
236
+ ```
237
+
238
+ ### Migration DSL
239
+
240
+ ```ruby
241
+ create_table :holdings do |t|
242
+ t.amount :amount
243
+ t.amount :default_amount, default: "USDC|1.25"
244
+ t.amount :fee, symbol: :SOL
245
+ t.amount :reserve, precision: 40
246
+ end
247
+ ```
248
+
249
+ This generates:
250
+
251
+ - `*_atomic` as `numeric(78, 0)` by default
252
+ - `*_symbol` as `string(10)` for multi-symbol amounts
253
+ - `precision:` override support for the atomic column
254
+
255
+ ### Model Macro
256
+
257
+ ```ruby
258
+ class Holding < ApplicationRecord
259
+ has_amount :amount
260
+ has_amount :fee, symbol: :SOL
261
+ end
262
+
263
+ holding = Holding.new
264
+ holding.amount = "USDC|1.50"
265
+ holding.fee = 0.25
266
+ ```
267
+
268
+ Writers accept:
269
+
270
+ - `Amount`
271
+ - `String` like `"USDC|1.50"`
272
+ - `Hash` payloads
273
+ - raw numeric values for fixed-symbol attributes only
274
+
275
+ Scopes:
276
+
277
+ ```ruby
278
+ Holding.where_amount("USDC|1.50")
279
+ Holding.where_amount_gt("USDC|1.00")
280
+ Holding.where_amount_gte("USDC|1.00")
281
+ Holding.where_amount_lt("USDC|5.00")
282
+ Holding.where_amount_lte("USDC|5.00")
283
+ Holding.where_amount_between("USDC|1.00", "USDC|5.00")
284
+ Holding.amount_in(:USDC)
285
+ ```
286
+
287
+ Suggested check constraint for multi-symbol amounts:
288
+
289
+ ```sql
290
+ ALTER TABLE holdings ADD CONSTRAINT amount_both_or_neither
291
+ CHECK ((amount_atomic IS NULL) = (amount_symbol IS NULL));
292
+ ```
293
+
294
+ ### SQLite Note
295
+
296
+ The adapter supports SQLite for general integration tests and development, but SQLite does not preserve `DECIMAL(78,0)` values above 64-bit range exactly under ActiveRecord's default numeric handling. For exact wei-scale persistence, use PostgreSQL or another database with true arbitrary-precision numeric behavior.
297
+
298
+ ### PostgreSQL Dummy App
299
+
300
+ A minimal Rails dummy app lives under `test/dummy` for PostgreSQL-backed integration testing.
301
+
302
+ Run the default suite:
303
+
304
+ ```bash
305
+ bundle exec rake
306
+ ```
307
+
308
+ Run the PostgreSQL integration test explicitly:
309
+
310
+ ```bash
311
+ AMOUNTS_POSTGRES_URL=postgresql://localhost/amounts_test \
312
+ bundle exec ruby -Ilib:test test/postgresql_integration_test.rb
313
+ ```
314
+
315
+ If `AMOUNTS_POSTGRES_URL` is not set, the PostgreSQL test file skips cleanly.
316
+
317
+ Open a console against the dummy app:
318
+
319
+ ```bash
320
+ AMOUNTS_POSTGRES_URL=postgresql://localhost/amounts_test \
321
+ test/dummy/bin/rails console
322
+ ```
323
+
324
+ ## Testing
325
+
326
+ The gem ships opt-in RSpec matchers for app-level specs:
327
+
328
+ ```ruby
329
+ # spec/spec_helper.rb
330
+ require "amount/rspec"
331
+ require "amount/active_record/rspec"
332
+ ```
333
+
334
+ Core matcher examples:
335
+
336
+ ```ruby
337
+ expect(holding.amount).to eq_amount("USDC|1.50")
338
+ expect(holding.amount).to be_amount_of(:USDC)
339
+ expect(holding.amount).to be_positive_amount
340
+ expect(converted).to be_approximately_amount(:GOLD, "0.0042", within: "0.0001")
341
+ ```
342
+
343
+ ActiveRecord matcher examples:
344
+
345
+ ```ruby
346
+ expect(holding).to have_amount_column(:amount, "USDC|1.50")
347
+ expect(Holding.group(:amount_symbol).sum(:amount_atomic))
348
+ .to match_amounts(USDC: "10500.00", SOL: "12.5")
349
+ ```
350
+
351
+ The gem's own test suite runs both Minitest and RSpec:
352
+
353
+ ```bash
354
+ bundle exec rake
355
+ bundle exec rspec
356
+ ```
357
+
358
+ ## Releases
359
+
360
+ RubyGems publishing is intended to run from GitHub Releases using RubyGems trusted publishing.
361
+
362
+ Workflow:
363
+
364
+ - create and push a version tag such as `v0.0.1`
365
+ - publish a GitHub Release for that tag
366
+ - GitHub Actions runs `.github/workflows/release.yml`
367
+ - the workflow verifies the test suite and publishes the gem to RubyGems.org
368
+
369
+ RubyGems setup:
370
+
371
+ - on RubyGems.org, configure a trusted publisher for the `amounts` gem
372
+ - repository owner: `zarpay`
373
+ - repository name: `amounts`
374
+ - workflow filename: `release.yml`
375
+ - GitHub Actions environment: `release`
376
+
377
+ This uses OIDC trusted publishing, so no RubyGems API token needs to be stored in GitHub Actions. See the official RubyGems trusted publishing guide:
378
+
379
+ - https://guides.rubygems.org/trusted-publishing/
380
+
381
+ ## Compared to `money`
382
+
383
+ `money` is excellent for fiat currency workflows and has a mature ecosystem. `amounts` takes a different tradeoff:
384
+
385
+ | Concern | `money` | `amounts` |
386
+ | --- | --- | --- |
387
+ | Internal storage | integer subunits | arbitrary-precision atomic integer |
388
+ | Non-fiat tokens | awkward at high decimals | first-class |
389
+ | Cross-type math | money-oriented exchange features | explicit directional rates only |
390
+ | Display units | currency formatting | arbitrary per-type display scaling |
391
+ | Rails dependency | common usage path | optional adapter only |
392
+
393
+ ## Contributing
394
+
395
+ 1. Install dependencies with `bin/setup`
396
+ 2. Run `bundle exec rake`
397
+ 3. Keep the core gem Rails-agnostic
398
+ 4. Add tests for behavioral changes
399
+
400
+ ## License
401
+
402
+ Released under the MIT License. See [LICENSE.txt](LICENSE.txt).
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+ require "rspec/core/rake_task"
6
+ require "rubocop/rake_task"
7
+ Rake::TestTask.new(:test) do |task|
8
+ task.libs << "lib"
9
+ task.libs << "test"
10
+ task.pattern = "test/**/*_test.rb"
11
+ end
12
+
13
+ RuboCop::RakeTask.new(:rubocop)
14
+ RSpec::Core::RakeTask.new(:spec)
15
+
16
+ task lint: %i[rubocop]
17
+
18
+ task default: %i[test spec rubocop]
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "amount"
6
+ require "irb"
7
+
8
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ bundle install
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Amount
4
+ module ActiveRecord
5
+ # Validates model-level business rules for `has_amount` attributes.
6
+ #
7
+ # Structural integrity is still handled by `has_amount` itself. This
8
+ # validator adds declarative Rails validations such as symbol checks and
9
+ # comparison constraints.
10
+ #
11
+ # @example Requiring a specific symbol
12
+ # class Holding < ApplicationRecord
13
+ # has_amount :amount
14
+ # validates :amount, amount: { symbol: :USDC }
15
+ # end
16
+ #
17
+ # @example Applying numeric thresholds to a fixed-symbol amount
18
+ # class FeeSchedule < ApplicationRecord
19
+ # has_amount :fee, symbol: :SOL
20
+ # validates :fee, amount: { greater_than_or_equal_to: 0 }
21
+ # end
22
+ class AmountValidator < ::ActiveModel::EachValidator
23
+ COMPARATORS = {
24
+ greater_than: :>,
25
+ greater_than_or_equal_to: :>=,
26
+ less_than: :<,
27
+ less_than_or_equal_to: :<=
28
+ }.freeze
29
+
30
+ def validate_each(record, attribute, value)
31
+ return if pending_assignment_error?(record, attribute)
32
+ return if value.nil?
33
+
34
+ validate_amount_instance(record, attribute, value)
35
+ validate_symbol(record, attribute, value)
36
+ validate_comparators(record, attribute, value)
37
+ end
38
+
39
+ private
40
+
41
+ def validate_amount_instance(record, attribute, value)
42
+ return if value.is_a?(::Amount)
43
+
44
+ record.errors.add(attribute, "must be an Amount")
45
+ end
46
+
47
+ def validate_symbol(record, attribute, value)
48
+ expected_symbol = options[:symbol]&.to_sym
49
+ return unless expected_symbol
50
+ return if value.symbol == expected_symbol
51
+
52
+ record.errors.add(attribute, "must have symbol #{expected_symbol}")
53
+ end
54
+
55
+ def validate_comparators(record, attribute, value)
56
+ COMPARATORS.each do |option_name, operator|
57
+ next unless options.key?(option_name)
58
+
59
+ other = coerce_comparison_amount(record, attribute, options.fetch(option_name))
60
+ next unless other
61
+
62
+ compare_amounts(record, attribute, value, operator, other, option_name)
63
+ rescue ::Amount::Error, ArgumentError => error
64
+ record.errors.add(attribute, "has invalid #{option_name} constraint: #{error.message}")
65
+ end
66
+ end
67
+
68
+ def coerce_comparison_amount(record, attribute, candidate)
69
+ definition = fetch_definition(record, attribute)
70
+ return candidate if candidate.is_a?(::Amount)
71
+
72
+ definition.type.cast(candidate)
73
+ end
74
+
75
+ def compare_amounts(record, attribute, value, operator, other, option_name)
76
+ comparison = value <=> other
77
+ if comparison.nil?
78
+ record.errors.add(attribute, "cannot compare #{value.symbol} to #{other.symbol} for #{option_name}")
79
+ return
80
+ end
81
+
82
+ return if comparison.public_send(operator, 0)
83
+
84
+ record.errors.add(attribute, failure_message(option_name, other))
85
+ end
86
+
87
+ def failure_message(option_name, other)
88
+ case option_name
89
+ when :greater_than
90
+ "must be greater than #{other}"
91
+ when :greater_than_or_equal_to
92
+ "must be greater than or equal to #{other}"
93
+ when :less_than
94
+ "must be less than #{other}"
95
+ when :less_than_or_equal_to
96
+ "must be less than or equal to #{other}"
97
+ else
98
+ "is invalid"
99
+ end
100
+ end
101
+
102
+ def fetch_definition(record, attribute)
103
+ record.class.amount_attribute_definitions.fetch(attribute.to_sym)
104
+ rescue KeyError
105
+ raise ArgumentError, "#{record.class.name}##{attribute} is not declared with has_amount"
106
+ end
107
+
108
+ def pending_assignment_error?(record, attribute)
109
+ record.send(:pending_amount_assignment_errors).key?(attribute.to_sym)
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ ::AmountValidator = Amount::ActiveRecord::AmountValidator unless defined?(::AmountValidator)