money_attribute 1.2.0 → 1.3.0
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/README.md +166 -72
- data/Rakefile +31 -6
- data/lib/generators/money_attribute/initializer_generator.rb +10 -0
- data/lib/money_attribute/attribute_spec.rb +9 -3
- data/lib/money_attribute/attribute_spec_registry.rb +63 -5
- data/lib/money_attribute/column_type_validations.rb +47 -0
- data/lib/money_attribute/converter.rb +18 -3
- data/lib/money_attribute/core_ext/numeric.rb +10 -1
- data/lib/money_attribute/core_ext/string.rb +12 -2
- data/lib/money_attribute/form_builder_extension.rb +48 -3
- data/lib/money_attribute/macro.rb +96 -7
- data/lib/money_attribute/migration_extensions/helper.rb +61 -1
- data/lib/money_attribute/migration_extensions/schema_statements.rb +89 -9
- data/lib/money_attribute/migration_extensions/table_definition.rb +68 -11
- data/lib/money_attribute/money_amount.rb +30 -2
- data/lib/money_attribute/query/amount_condition.rb +139 -2
- data/lib/money_attribute/query/amount_order.rb +9 -3
- data/lib/money_attribute/query/currency_condition.rb +8 -2
- data/lib/money_attribute/query/helpers.rb +28 -5
- data/lib/money_attribute/query/pick.rb +11 -8
- data/lib/money_attribute/query/pluck.rb +11 -15
- data/lib/money_attribute/query/sum.rb +15 -3
- data/lib/money_attribute/query.rb +16 -11
- data/lib/money_attribute/railtie.rb +40 -0
- data/lib/money_attribute/type.rb +6 -2
- data/lib/money_attribute/version.rb +2 -1
- data/lib/money_attribute.rb +1 -0
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4101988f3c8acc6d5a4810519fa74d1fd27feaba2114ee047ecd755da3229c91
|
|
4
|
+
data.tar.gz: 2a7071882d41791e33c3d189bd5db42ea39b434c597f64a3c5b6ae88facd7e82
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eac6fb9ca6ef50c717fc51b8c4aac7977e425787ea98032ae02e66091f74a66158b1f19f401b5b15df3cf8dc7a402aba942e9c7b55f1dc054ec2c617114d2c87
|
|
7
|
+
data.tar.gz: 2216348ddfa7df7d97ddacfe12d15a895d2e1c9ac28392719c14add69741a177ab5064b5a8da5befde3ec9b28a028e37924b4f3db433a2a70af626af5e619ddc
|
data/README.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
[](https://github.com/gferraz/money-attribute/actions/workflows/ci.yml)
|
|
4
4
|
[](https://badge.fury.io/rb/money_attribute)
|
|
5
5
|
|
|
6
|
-
Store and read Active Record attributes as `
|
|
6
|
+
Store and read Active Record attributes as `Money` objects with no manual serialization.
|
|
7
7
|
|
|
8
|
-
`money_attribute` uses two DB columns (amount + currency) for per-row multi-currency data.
|
|
8
|
+
`money_attribute` uses two DB columns (amount + currency) for per-row multi-currency data.
|
|
9
9
|
|
|
10
10
|
```ruby
|
|
11
11
|
class Product < ApplicationRecord
|
|
@@ -32,8 +32,7 @@ p.price * 2 # => [USD 24.88]
|
|
|
32
32
|
- [Convenience methods](#convenience-methods)
|
|
33
33
|
- [Form helpers](#form-helpers)
|
|
34
34
|
- [Roadmap](#roadmap)
|
|
35
|
-
- [Development
|
|
36
|
-
- [License](#license)
|
|
35
|
+
- [Development](#development)
|
|
37
36
|
|
|
38
37
|
## Quick start
|
|
39
38
|
|
|
@@ -62,23 +61,23 @@ class Product < ApplicationRecord
|
|
|
62
61
|
end
|
|
63
62
|
```
|
|
64
63
|
|
|
65
|
-
That's it. `Product.new(price: 12.dollars).price` is a `
|
|
64
|
+
That's it. `Product.new(price: 12.dollars).price` is a `Money`.
|
|
66
65
|
|
|
67
66
|
## Why MoneyAttribute?
|
|
68
67
|
|
|
69
|
-
- **No serialization boilerplate** — declare once, read/write `
|
|
68
|
+
- **No serialization boilerplate** — declare once, read/write `Money` everywhere.
|
|
70
69
|
- **Integer or decimal columns** — auto-detects the column type and adjusts serialization (e.g. integer stores cents, decimal stores unit value).
|
|
71
|
-
- **Normalizes everything** — pass a number, string, or `
|
|
72
|
-
- **Currency
|
|
70
|
+
- **Normalizes everything** — pass a number, string, or `Money`; always get a `Money` back.
|
|
71
|
+
- **Currency handling** — each record stores its own currency and accepts registered currencies at assignment time.
|
|
73
72
|
- **Built on Rails primitives** — uses `ActiveRecord::Type`, `composed_of`, and `normalizes` under the hood. No monkey-patching of core classes.
|
|
74
73
|
|
|
75
74
|
### At a glance — vs money-rails
|
|
76
75
|
|
|
77
76
|
| Feature | MoneyAttribute | money-rails |
|
|
78
77
|
|---|---|---|
|
|
79
|
-
| **Declare** | `t.money_attribute :price` / `money_attribute :price`
|
|
78
|
+
| **Declare** | `t.money_attribute :price` / `money_attribute :price` | `monetize :price_cents` |
|
|
80
79
|
| **Column types** | `integer`, `decimal`, `bigint` — auto-detected | `integer` cents only |
|
|
81
|
-
| **Storage
|
|
80
|
+
| **Storage mode** | Composite (amount+currency) | Single cents column, composite (cents+currency) |
|
|
82
81
|
| **Decimal columns** | Native — `t.decimal :price` | Not supported — must convert to cents manually |
|
|
83
82
|
| **Multi-currency** | `money_attribute :price` (convention: `<name>_amount` + `<name>_currency`) | `monetize :price_cents, with_currency: :price_currency` |
|
|
84
83
|
| **Rails integration** | `ActiveRecord::Type` + `composed_of` — no monkey-patches | `monetize` overrides reader/writer methods |
|
|
@@ -93,7 +92,7 @@ For a detailed side-by-side comparison, see [COMPARISON.md](COMPARISON.md).
|
|
|
93
92
|
|
|
94
93
|
- Ruby 3.3+
|
|
95
94
|
- Rails 7.1.3.2+
|
|
96
|
-
- [Minting](https://github.com/gferraz/minting) 2.
|
|
95
|
+
- [Minting](https://github.com/gferraz/minting) 2.1+
|
|
97
96
|
|
|
98
97
|
## Installation
|
|
99
98
|
|
|
@@ -150,7 +149,33 @@ class AddPriceToProducts < ActiveRecord::Migration[8.1]
|
|
|
150
149
|
end
|
|
151
150
|
```
|
|
152
151
|
|
|
153
|
-
###
|
|
152
|
+
### Default column names
|
|
153
|
+
|
|
154
|
+
For `money_attribute :price`, the default columns are `price` for the amount
|
|
155
|
+
and `price_currency` for the currency. The migration helper and model macro use
|
|
156
|
+
the same mapping:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
create_table :products do |t|
|
|
160
|
+
t.money_attribute :price
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
class Product < ApplicationRecord
|
|
164
|
+
money_attribute :price
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
This creates and maps to:
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
price
|
|
172
|
+
price_currency
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The amount column defaults to `decimal(20,4)` and the currency column defaults
|
|
176
|
+
to `string(16)`.
|
|
177
|
+
|
|
178
|
+
### Naming options
|
|
154
179
|
|
|
155
180
|
**`money_attribute` (composite):**
|
|
156
181
|
|
|
@@ -159,7 +184,7 @@ end
|
|
|
159
184
|
| `t.money_attribute :price` | `price` decimal(20,4) + `price_currency` string(16) | `money_attribute :price` |
|
|
160
185
|
| `t.money_attribute :price_amount` | `price_amount` decimal(20,4) + `price_currency` string(16) | `money_attribute :price` |
|
|
161
186
|
| `t.money_attribute :price, amount: { type: :fiat_integer }` | `price` bigint + `price_currency` string(16) | `money_attribute :price` |
|
|
162
|
-
| `t.money_attribute :price, amount: { column: :a }, currency: { column: :c }` | `a` + `c` | `money_attribute :price,
|
|
187
|
+
| `t.money_attribute :price, amount: { column: :a }, currency: { column: :c }` | `a` + `c` | `money_attribute :price, amount: { column: :a }, currency: { column: :c }` |
|
|
163
188
|
| `t.money_attribute :price, currency: { limit: 5 }` | `price` decimal(20,4) + `price_currency` string(5) | `money_attribute :price` |
|
|
164
189
|
| `t.remove_money_attribute :price` | Removes `price` + `price_currency` | `money_attribute :price` |
|
|
165
190
|
|
|
@@ -197,7 +222,7 @@ I18n.locale = :'pt-BR'
|
|
|
197
222
|
Money.from(1234.56, 'USD').to_s # => "$1.234,56"
|
|
198
223
|
```
|
|
199
224
|
|
|
200
|
-
The locale backend reads `number.currency.format` from your I18n translations and maps Rails format syntax (`%n` for amount, `%u` for unit) to `
|
|
225
|
+
The locale backend reads `number.currency.format` from your I18n translations and maps Rails format syntax (`%n` for amount, `%u` for unit) to `Money#to_s`. If the translation key is missing (no locale file for that language), it falls back to hardcoded defaults (`.` decimal, `,` thousand, `%<symbol>s%<amount>f` format).
|
|
201
226
|
|
|
202
227
|
You can configure per-sign formatting by adding `positive`, `negative`, and `zero` keys to your locale:
|
|
203
228
|
|
|
@@ -282,38 +307,55 @@ Order.new(total: 19.99.to_money('USD')).total_amount # => 1999
|
|
|
282
307
|
|
|
283
308
|
## Custom column names
|
|
284
309
|
|
|
285
|
-
If your columns
|
|
310
|
+
If your columns do not follow the default mapping, use the same nested column
|
|
311
|
+
options as the migration helper:
|
|
286
312
|
|
|
287
313
|
```ruby
|
|
288
314
|
class Invoice < ApplicationRecord
|
|
289
|
-
money_attribute :total,
|
|
290
|
-
amount:
|
|
291
|
-
currency: :currency_code
|
|
292
|
-
}
|
|
315
|
+
money_attribute :total,
|
|
316
|
+
amount: { column: :total_amount },
|
|
317
|
+
currency: { column: :currency_code }
|
|
293
318
|
end
|
|
294
319
|
```
|
|
295
320
|
|
|
296
|
-
|
|
321
|
+
You can provide only one nested option; the other falls back to the default
|
|
322
|
+
mapping. The older `mapping:` syntax remains supported (but it will eventually be deprecated):
|
|
297
323
|
|
|
298
324
|
```ruby
|
|
299
325
|
class Invoice < ApplicationRecord
|
|
300
|
-
money_attribute :total,
|
|
301
|
-
|
|
326
|
+
money_attribute :total, amount: { column: :total_amount }
|
|
327
|
+
money_attribute :subtotal, mapping: { amount: :subtotal }
|
|
302
328
|
end
|
|
303
329
|
```
|
|
304
330
|
|
|
331
|
+
Model declarations accept only the nested `:column` option. Migration-only
|
|
332
|
+
options such as `:type`, `:null`, and `:default` belong in the migration.
|
|
333
|
+
|
|
305
334
|
## Column resolution
|
|
306
335
|
|
|
307
|
-
`money_attribute :name` is always composite.
|
|
336
|
+
`money_attribute :name` is always composite. Columns are resolved in two phases:
|
|
308
337
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
|
312
|
-
|
|
313
|
-
|
|
|
314
|
-
|
|
|
338
|
+
**Phase 1 — Default mapping** is determined by which columns exist:
|
|
339
|
+
|
|
340
|
+
| Condition | Default columns |
|
|
341
|
+
|---|---|
|
|
342
|
+
| `name_currency` exists AND `name` exists | `name` + `name_currency` |
|
|
343
|
+
| `name == 'amount'` AND `currency` exists | `amount` + `currency` |
|
|
344
|
+
| Otherwise | `<name>_amount` + `<name>_currency` |
|
|
315
345
|
|
|
316
|
-
|
|
346
|
+
The first row is the canonical mapping used by the migration helpers. The
|
|
347
|
+
final row supports existing schemas that use the `<name>_amount` /
|
|
348
|
+
`<name>_currency` convention. For example, `money_attribute :price` also maps
|
|
349
|
+
to `price_amount` + `price_currency` when those are the available columns.
|
|
350
|
+
|
|
351
|
+
**Phase 2 — Override** via `mapping:` is merged on top of the default. Missing keys inherit from the default mapping:
|
|
352
|
+
|
|
353
|
+
```ruby
|
|
354
|
+
money_attribute :total, mapping: { amount: :total_amount }
|
|
355
|
+
# currency falls back to default => :total_currency
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Raises `ArgumentError` if the resolved columns don't exist. For single-column fixed-currency attributes, see the [advanced `money_amount` section](#advanced-money_amount-single-column-fixed-currency).
|
|
317
359
|
|
|
318
360
|
**Example**
|
|
319
361
|
|
|
@@ -325,7 +367,6 @@ create_table :financial_transactions do |t|
|
|
|
325
367
|
t.string :discount_currency, limit: 3
|
|
326
368
|
t.decimal :price_amount
|
|
327
369
|
t.string :price_currency, limit: 3
|
|
328
|
-
t.bigint :tax
|
|
329
370
|
t.decimal :total_amount
|
|
330
371
|
t.string :currency_code, limit: 3
|
|
331
372
|
end
|
|
@@ -337,7 +378,6 @@ class FinancialTransaction < ApplicationRecord
|
|
|
337
378
|
money_attribute :discount # step 2: discount(int) + discount_currency
|
|
338
379
|
money_attribute :price # step 4: price_amount + price_currency
|
|
339
380
|
money_attribute :total, mapping: { amount: :total_amount, currency: :currency_code } # step 1: explicit
|
|
340
|
-
money_amount :tax # single-column, fixed-currency (uses default currency)
|
|
341
381
|
end
|
|
342
382
|
```
|
|
343
383
|
|
|
@@ -348,7 +388,7 @@ end
|
|
|
348
388
|
Multi-currency (`money_attribute`) attributes support equality queries via `composed_of`:
|
|
349
389
|
|
|
350
390
|
```ruby
|
|
351
|
-
Offer.where(price: 10.
|
|
391
|
+
Offer.where(price: 10.euros)
|
|
352
392
|
```
|
|
353
393
|
|
|
354
394
|
For comparisons, use the backing columns directly:
|
|
@@ -358,15 +398,25 @@ Offer.where(price_amount: 10..20, price_currency: 'EUR')
|
|
|
358
398
|
Offer.where('price_amount > ? AND price_currency = ?', 10, 'EUR')
|
|
359
399
|
```
|
|
360
400
|
|
|
361
|
-
|
|
401
|
+
### Composite query matrix
|
|
362
402
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
403
|
+
For `money_attribute`, use native Rails queries for simple equality. Use the
|
|
404
|
+
money-aware helpers when working with `Money` objects or when you need an
|
|
405
|
+
explicit currency filter alongside an amount query:
|
|
406
|
+
|
|
407
|
+
| Operation | Native Rails | Money-aware helper |
|
|
408
|
+
|---|---|---|
|
|
409
|
+
| Equality | `where(price: 10.euros)` | `where_amount(price: 10.euros)` |
|
|
410
|
+
| `IN` | Query backing columns directly | `where_amount(price: [10.euros, 20.euros])` |
|
|
411
|
+
| Range/comparison | Query backing columns directly | `where_currency(price: 'EUR').where_amount(price: 10.euros..20.euros)` |
|
|
412
|
+
| Currency filter | `where(price_currency: 'EUR')` | `where_currency(price: 'EUR')` |
|
|
413
|
+
| Ordering | Query backing columns directly | `order_by_amount(price: :desc)` |
|
|
414
|
+
| Pluck | Returns backing column values | `pluck_amount(:price)` returns `Money` |
|
|
415
|
+
| Pick | Returns backing column values | `pick_amount(:price)` returns `Money` |
|
|
416
|
+
| Sum | Group and sum backing columns directly | `sum_amount(:price)` groups by currency |
|
|
417
|
+
|
|
418
|
+
`where_amount` accepts `Money` values and converts them to the backing amount
|
|
419
|
+
unit. Pair it with `where_currency` when the currency must be constrained.
|
|
370
420
|
|
|
371
421
|
### Money-aware query helpers
|
|
372
422
|
|
|
@@ -377,35 +427,28 @@ For multi-currency attributes, manually decomposing columns is tedious. The quer
|
|
|
377
427
|
Filters by amount value. Accepts a scalar, Range, or Array.
|
|
378
428
|
|
|
379
429
|
```ruby
|
|
380
|
-
Offer.where_amount(price: 10)
|
|
381
|
-
Offer.where_amount(price: [10, 30])
|
|
382
|
-
Offer.where_amount(price: 10..100)
|
|
383
|
-
Offer.where_amount(price: 10...100)
|
|
430
|
+
Offer.where_amount(price: 10.euros) # equality
|
|
431
|
+
Offer.where_amount(price: [10.euros, 30.euros]) # IN
|
|
432
|
+
Offer.where_amount(price: 10.euros..100.euros) # BETWEEN (inclusive)
|
|
433
|
+
Offer.where_amount(price: 10.euros...100.euros) # BETWEEN (exclusive upper bound)
|
|
384
434
|
```
|
|
385
435
|
|
|
386
|
-
|
|
436
|
+
For a currency-specific range, constrain the currency explicitly:
|
|
387
437
|
|
|
388
438
|
```ruby
|
|
389
439
|
Offer.create!(price: 10.euros)
|
|
390
|
-
Offer.create!(price: 50.
|
|
440
|
+
Offer.create!(price: 50.euros)
|
|
391
441
|
|
|
392
|
-
Offer.where_amount(price: 10..50)
|
|
442
|
+
Offer.where_currency(price: 'EUR').where_amount(price: 10.euros..50.euros) # => both records
|
|
393
443
|
```
|
|
394
444
|
|
|
395
|
-
For integer (subunit) columns, pass `
|
|
445
|
+
For integer (subunit) columns, pass `Money` objects directly — subunit conversion is handled automatically:
|
|
396
446
|
|
|
397
447
|
```ruby
|
|
398
|
-
FinancialTransaction.where_amount(amount: [10.dollars,
|
|
448
|
+
FinancialTransaction.where_amount(amount: [10.dollars, 20.dollars])
|
|
399
449
|
FinancialTransaction.where_amount(amount: 10.dollars..100.dollars)
|
|
400
450
|
```
|
|
401
451
|
|
|
402
|
-
For decimal columns, raw numbers work:
|
|
403
|
-
|
|
404
|
-
```ruby
|
|
405
|
-
SimpleOffer.where_amount(price: 50)
|
|
406
|
-
SimpleOffer.where_amount(price: 10..100)
|
|
407
|
-
```
|
|
408
|
-
|
|
409
452
|
#### `where_currency`
|
|
410
453
|
|
|
411
454
|
Filters by currency code. Composite attributes only — raises `ArgumentError` for single-column attributes.
|
|
@@ -456,13 +499,27 @@ SimpleOffer.sum_amount(:price)
|
|
|
456
499
|
# => [BRL 60.00] (single-column always returns one Money)
|
|
457
500
|
|
|
458
501
|
Offer.none.sum_amount(:price)
|
|
459
|
-
# => [
|
|
502
|
+
# => [BRL 0.00] (empty result returns zero Money)
|
|
460
503
|
```
|
|
461
504
|
|
|
462
505
|
### Notes
|
|
463
506
|
|
|
464
507
|
All query helpers raise `ArgumentError` for non-money attributes. Internally, money attribute metadata is registered per model class. The same attribute name can be used safely in different models, but subclasses do not automatically inherit a parent model's registered money attributes.
|
|
465
508
|
|
|
509
|
+
### Introspection
|
|
510
|
+
|
|
511
|
+
Use `money_attribute?` and `money_attribute_kind` when building generic model
|
|
512
|
+
or form code:
|
|
513
|
+
|
|
514
|
+
```ruby
|
|
515
|
+
Product.money_attribute?(:price) # => true
|
|
516
|
+
Product.money_attribute_kind(:price) # => :composite or :single
|
|
517
|
+
Product.money_attribute?(:unknown) # => false
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
`money_attribute_kind` returns `:composite` for `money_attribute`, `:single`
|
|
521
|
+
for `money_amount`, and `nil` for undeclared attributes.
|
|
522
|
+
|
|
466
523
|
## Convenience methods
|
|
467
524
|
|
|
468
525
|
MoneyAttribute adds small helpers on `Numeric` and `String`:
|
|
@@ -477,24 +534,53 @@ MoneyAttribute adds small helpers on `Numeric` and `String`:
|
|
|
477
534
|
|
|
478
535
|
## Form helpers
|
|
479
536
|
|
|
480
|
-
MoneyAttribute adds `money_field`
|
|
537
|
+
MoneyAttribute adds `money_field` to Rails form builders for composite money attributes. It renders a text input with the locale-formatted money string.
|
|
481
538
|
|
|
482
539
|
```erb
|
|
483
540
|
<%= form_with model: @product do |form| %>
|
|
484
541
|
<%= form.label :price %>
|
|
485
542
|
<%= form.money_field :price %> <!-- text input, e.g. "$1,234.56" -->
|
|
486
|
-
|
|
487
|
-
<%= form.label :tax %>
|
|
488
|
-
<%= form.money_amount_field :tax %> <!-- number input, e.g. "1234.56" -->
|
|
489
543
|
<% end %>
|
|
490
544
|
```
|
|
491
545
|
|
|
492
|
-
|
|
546
|
+
The field submits a locale-formatted string under the normal model parameter:
|
|
493
547
|
|
|
494
|
-
|
|
548
|
+
```ruby
|
|
549
|
+
# app/controllers/products_controller.rb
|
|
550
|
+
def create
|
|
551
|
+
@product = Product.new(product_params)
|
|
552
|
+
|
|
553
|
+
if @product.save
|
|
554
|
+
redirect_to @product
|
|
555
|
+
else
|
|
556
|
+
render :new, status: :unprocessable_entity
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
private
|
|
561
|
+
|
|
562
|
+
def product_params
|
|
563
|
+
params.require(:product).permit(:name, :price)
|
|
564
|
+
end
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
The model normalizes the submitted string through `MoneyAttribute::Converter`.
|
|
568
|
+
For a composite attribute, provide a separate currency input or set the
|
|
569
|
+
currency explicitly when the form must accept a currency different from the
|
|
570
|
+
configured default.
|
|
571
|
+
|
|
572
|
+
### Advanced: `money_amount` (single-column, fixed-currency)
|
|
573
|
+
|
|
574
|
+
`money_amount` is an advanced compatibility utility for schemas where one
|
|
575
|
+
numeric column stores amounts in one fixed currency. It is not suitable for
|
|
576
|
+
per-row or multi-currency data. New applications should normally use
|
|
577
|
+
`money_attribute`.
|
|
495
578
|
|
|
496
579
|
The accessor name must match the column name. `money_amount` does not support custom column mapping.
|
|
497
580
|
|
|
581
|
+
Use `money_amount_field` when rendering a form for this mode; it renders a
|
|
582
|
+
number input with the raw decimal value.
|
|
583
|
+
|
|
498
584
|
#### Migration helpers
|
|
499
585
|
|
|
500
586
|
| Method | Action |
|
|
@@ -505,9 +591,9 @@ The accessor name must match the column name. `money_amount` does not support cu
|
|
|
505
591
|
Default column: `decimal(20,4)`. The top-level `type:` shortcut selects the column type:
|
|
506
592
|
|
|
507
593
|
```ruby
|
|
508
|
-
t.money_amount :price
|
|
594
|
+
t.money_amount :price # decimal(20,4)
|
|
509
595
|
t.money_amount :btc_balance, type: :crypto_decimal # decimal(36,18)
|
|
510
|
-
t.money_amount :qty,
|
|
596
|
+
t.money_amount :qty, type: :fiat_integer # bigint
|
|
511
597
|
```
|
|
512
598
|
|
|
513
599
|
#### Naming
|
|
@@ -545,7 +631,19 @@ money_amount :price
|
|
|
545
631
|
|
|
546
632
|
#### Querying
|
|
547
633
|
|
|
548
|
-
Fixed-currency attributes support full Rails-native querying
|
|
634
|
+
Fixed-currency attributes support full Rails-native querying. The helper methods
|
|
635
|
+
are also available when a consistent query interface is useful:
|
|
636
|
+
|
|
637
|
+
| Operation | Native Rails | Money-aware helper |
|
|
638
|
+
|---|---|---|
|
|
639
|
+
| Equality | `where(price: 10.reais)` | `where_amount(price: 10.reais)` |
|
|
640
|
+
| `IN` | `where(price: [10.reais, 20.reais])` | `where_amount(price: [10.reais, 20.reais])` |
|
|
641
|
+
| Range/comparison | `where(price: 10.reais..20.reais)` | `where_amount(price: 10.reais..20.reais)` |
|
|
642
|
+
| Currency filter | Not applicable | Raises `ArgumentError` |
|
|
643
|
+
| Ordering | `order(price: :desc)` | `order_by_amount(price: :desc)` |
|
|
644
|
+
| Pluck | `pluck(:price)` | `pluck_amount(:price)` |
|
|
645
|
+
| Pick | `pick(:price)` | `pick_amount(:price)` |
|
|
646
|
+
| Sum | `sum(:price)` | `sum_amount(:price)` |
|
|
549
647
|
|
|
550
648
|
## Roadmap
|
|
551
649
|
|
|
@@ -564,8 +662,4 @@ The dummy Rails app under `test/dummy` exercises the engine in a full Rails envi
|
|
|
564
662
|
|
|
565
663
|
## Contributing
|
|
566
664
|
|
|
567
|
-
Bug reports
|
|
568
|
-
|
|
569
|
-
## License
|
|
570
|
-
|
|
571
|
-
[MIT](MIT-LICENSE)
|
|
665
|
+
Bug reports welcome at [gferraz/money-attribute](https://github.com/gferraz/money-attribute).
|
data/Rakefile
CHANGED
|
@@ -15,7 +15,7 @@ Rake::TestTask.new(:test_run) do |t|
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
desc 'Migrate test database'
|
|
18
|
-
task :test_db_migrate do
|
|
18
|
+
task :test_db_migrate do
|
|
19
19
|
sh({ 'RAILS_ENV' => 'test' }, 'bin/rails', 'db:migrate', chdir: 'test/dummy', %i[out err] => File::NULL)
|
|
20
20
|
end
|
|
21
21
|
|
|
@@ -27,13 +27,13 @@ task test: %i[test_db_migrate test_run]
|
|
|
27
27
|
%w[postgresql mysql2].each do |adapter|
|
|
28
28
|
namespace :test do
|
|
29
29
|
desc "Migrate test database (#{adapter})"
|
|
30
|
-
task "db_migrate:#{adapter}" do
|
|
30
|
+
task "db_migrate:#{adapter}" do
|
|
31
31
|
sh({ 'RAILS_ENV' => 'test', 'DATABASE_ADAPTER' => adapter },
|
|
32
32
|
'bin/rails', 'db:migrate', chdir: 'test/dummy', %i[out err] => File::NULL)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
desc "Run tests against #{adapter}"
|
|
36
|
-
task adapter do
|
|
36
|
+
task adapter do
|
|
37
37
|
Rake::Task[:"test:db_migrate:#{adapter}"].invoke
|
|
38
38
|
sh({ 'DATABASE_ADAPTER' => adapter }, 'bundle', 'exec', 'rake', 'test_run')
|
|
39
39
|
end
|
|
@@ -41,7 +41,7 @@ task test: %i[test_db_migrate test_run]
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
desc 'Run tests against all adapters (sqlite3, postgresql, mysql2)'
|
|
44
|
-
task 'test:all' do
|
|
44
|
+
task 'test:all' do
|
|
45
45
|
Rake::Task[:test].invoke
|
|
46
46
|
%w[postgresql mysql2].each do |adapter|
|
|
47
47
|
Rake::Task[:"test:#{adapter}"].invoke
|
|
@@ -50,7 +50,7 @@ end
|
|
|
50
50
|
|
|
51
51
|
# --- Benchmark ---
|
|
52
52
|
|
|
53
|
-
desc 'Run money_attribute vs money-rails benchmark'
|
|
53
|
+
desc 'Run money_attribute vs plain Rails vs money-rails benchmark'
|
|
54
54
|
task bench: :test_db_migrate do
|
|
55
55
|
puts
|
|
56
56
|
puts '=' * 80
|
|
@@ -59,6 +59,13 @@ task bench: :test_db_migrate do
|
|
|
59
59
|
sh({ 'RAILS_ENV' => 'test', 'BENCH_SIDE' => 'minting' },
|
|
60
60
|
'bundle', 'exec', 'ruby', 'benchmark/comparison.rb')
|
|
61
61
|
|
|
62
|
+
puts
|
|
63
|
+
puts '=' * 80
|
|
64
|
+
puts 'plain Rails (raw columns, no monetization)'
|
|
65
|
+
puts '=' * 80
|
|
66
|
+
sh({ 'RAILS_ENV' => 'test', 'BENCH_SIDE' => 'plain' },
|
|
67
|
+
'bundle', 'exec', 'ruby', 'benchmark/comparison.rb')
|
|
68
|
+
|
|
62
69
|
puts
|
|
63
70
|
puts '=' * 80
|
|
64
71
|
puts 'money-rails (money gem)'
|
|
@@ -69,6 +76,24 @@ task bench: :test_db_migrate do
|
|
|
69
76
|
end
|
|
70
77
|
|
|
71
78
|
desc 'Generate consolidated benchmark report (markdown)'
|
|
72
|
-
task 'bench:report' do
|
|
79
|
+
task 'bench:report' do
|
|
73
80
|
ruby 'benchmark/report.rb'
|
|
74
81
|
end
|
|
82
|
+
|
|
83
|
+
desc 'Profile hot paths with stackprof (MODE=string_query|pluck|read_cached|multi_record|arithmetic|all)'
|
|
84
|
+
task 'bench:profile' do
|
|
85
|
+
mode = ENV['MODE'] || 'all'
|
|
86
|
+
sh({ 'RAILS_ENV' => 'test' }, 'bundle', 'exec', 'ruby', 'benchmark/profile.rb', mode)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
desc 'Run focused performance roadmap benchmarks'
|
|
90
|
+
task 'bench:performance' do
|
|
91
|
+
sh({ 'RAILS_ENV' => 'test' }, 'bundle', 'exec', 'ruby', 'benchmark/performance.rb')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
desc 'Compare two focused performance benchmark result files'
|
|
95
|
+
task 'bench:performance:compare' do
|
|
96
|
+
baseline = ENV.fetch('BASELINE')
|
|
97
|
+
current = ENV.fetch('CURRENT')
|
|
98
|
+
sh 'bundle', 'exec', 'ruby', 'benchmark/compare_performance.rb', baseline, current
|
|
99
|
+
end
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module MoneyAttribute
|
|
4
|
+
# Generator namespace for money_attribute.
|
|
4
5
|
module Generators
|
|
6
|
+
# Rails generator that installs the money_attribute initializer.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# rails generate money_attribute:initializer
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
5
12
|
class InitializerGenerator < ::Rails::Generators::Base
|
|
6
13
|
source_root File.expand_path('../templates', __dir__)
|
|
7
14
|
|
|
8
15
|
desc 'Creates MoneyAttribute initializer.'
|
|
9
16
|
|
|
17
|
+
# Copies the initializer template into the application.
|
|
18
|
+
#
|
|
19
|
+
# @return [void]
|
|
10
20
|
def copy_initializer
|
|
11
21
|
copy_file 'money_attribute.rb', 'config/initializers/money_attribute.rb'
|
|
12
22
|
end
|
|
@@ -21,7 +21,9 @@ module MoneyAttribute
|
|
|
21
21
|
#
|
|
22
22
|
# Created by +money_attribute+ or +money_amount+ and stored in the class-level registry.
|
|
23
23
|
# Used by query helpers to resolve column names, build Money values, and generate SQL.
|
|
24
|
-
|
|
24
|
+
#
|
|
25
|
+
# @api private
|
|
26
|
+
AttributeSpec = Struct.new(:name, :kind, :amount_column, :currency_column, :amount_type, keyword_init: true) do
|
|
25
27
|
# @return [Boolean] +true+ when the spec describes a two-column (amount + currency) attribute.
|
|
26
28
|
def composite? = kind == :composite
|
|
27
29
|
|
|
@@ -32,7 +34,7 @@ module MoneyAttribute
|
|
|
32
34
|
#
|
|
33
35
|
# @return [Array<String>] two-element array for composite, one-element for single.
|
|
34
36
|
def columns
|
|
35
|
-
@columns ||= (composite? ? [
|
|
37
|
+
@columns ||= (composite? ? [amount_column, currency_column] : [amount_column]).freeze
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
# @return [Boolean] +true+ when the amount column stores subunits (bigint).
|
|
@@ -42,7 +44,9 @@ module MoneyAttribute
|
|
|
42
44
|
def amount_extractor = integer_amount? ? :subunits : :to_d
|
|
43
45
|
|
|
44
46
|
# @return [Hash{String => Symbol}] mapping suitable for +composed_of+.
|
|
45
|
-
def composed_of_mapping
|
|
47
|
+
def composed_of_mapping
|
|
48
|
+
@composed_of_mapping ||= { amount_column => amount_extractor, currency_column => :currency_code }.freeze
|
|
49
|
+
end
|
|
46
50
|
|
|
47
51
|
# @return [Proc] the constructor lambda used by +composed_of+ to instantiate Money values.
|
|
48
52
|
def constructor
|
|
@@ -54,6 +58,7 @@ module MoneyAttribute
|
|
|
54
58
|
# @param amount [Integer, BigDecimal, nil] the raw column value
|
|
55
59
|
# @param currency [String, Mint::Currency, nil] the currency code or object
|
|
56
60
|
# @return [Mint::Money, nil] the resolved money value, or nil when amount is nil
|
|
61
|
+
# @api private
|
|
57
62
|
def build_money(amount, currency)
|
|
58
63
|
return unless amount
|
|
59
64
|
return amount if amount.is_a?(Mint::Money)
|
|
@@ -68,6 +73,7 @@ module MoneyAttribute
|
|
|
68
73
|
#
|
|
69
74
|
# @param value [Mint::Money, Numeric] the query value
|
|
70
75
|
# @return [Integer, BigDecimal, Mint::Money] the normalized value
|
|
76
|
+
# @api private
|
|
71
77
|
def normalize_query_value(value)
|
|
72
78
|
return value unless integer_amount?
|
|
73
79
|
return value.subunits if value.is_a?(Mint::Money)
|